310 lines
10 KiB
Zig
310 lines
10 KiB
Zig
//! Module for loading persistent textures.
|
||
|
||
const Textures = @This();
|
||
const std = @import("std");
|
||
|
||
const media = @import("media");
|
||
|
||
const Atom = @import("../engine/Atom.zig").Atom;
|
||
const Engine = @import("../engine/Engine.zig");
|
||
const Texture = @import("../engine/Texture.zig");
|
||
|
||
pub const Id = enum(u16) {
|
||
// VOLATILE Synchronize explicit values with `init` implementation.
|
||
|
||
/// 1×1 texture with usage `.base_color`, whose texel is read as `vec4(1)`.
|
||
empty_base_color = 0,
|
||
/// 1×1 texture with usage `.emissive`, whose texel is read as `vec4(1)`.
|
||
empty_emissive = 1,
|
||
/// 1×1 texture with usage `.normal`, whose texel is read as
|
||
/// `vec4(0, 0, 1, 1)`.
|
||
empty_normal = 2,
|
||
/// 1×1 texture with usage `,occlusion_roughness_metallic`, whose texel is
|
||
/// read as `vec4(1)`.
|
||
empty_occlusion_roughness_metallic = 3,
|
||
_,
|
||
|
||
/// Cast an integer into an ID. This can produce an invalid ID.
|
||
pub fn fromInt(value: u16) Id {
|
||
return @enumFromInt(value);
|
||
}
|
||
|
||
/// Cast an index into an ID. This can produce an invalid ID. The caller
|
||
/// asserts that the index is not greater than the max ID value.
|
||
pub fn fromIndex(index: usize) Id {
|
||
std.debug.assert(index < max_textures);
|
||
return @enumFromInt(@as(u16, @intCast(index)));
|
||
}
|
||
|
||
/// Cast an index into an ID. This can produce an invalid ID. Returns an
|
||
/// error if the index is greater than the max ID value.
|
||
pub fn fromIndexSafe(index: usize) error{Overflow}!Id {
|
||
if (index >= max_textures) return error.Overflow;
|
||
return @enumFromInt(@as(u16, @intCast(index)));
|
||
}
|
||
|
||
/// Cast an ID into an integer.
|
||
pub fn toInt(self: Id) u16 {
|
||
return @intFromEnum(self);
|
||
}
|
||
};
|
||
|
||
pub const Key = struct {
|
||
/// Atom representing the filename of the texture.
|
||
filename: Atom,
|
||
/// Desired usage of the texture.
|
||
usage: Texture.Usage,
|
||
};
|
||
|
||
/// Maps a key value to a texture ID.
|
||
map: std.AutoHashMapUnmanaged(Key, Id),
|
||
/// Stores all `Texture` structs and maps a texture ID to a `Texture` struct.
|
||
array: std.ArrayList(Texture),
|
||
|
||
/// 4096 textures of usage `.base_color` and 16×16 dimensions should take 4 MiB
|
||
/// in VRAM.
|
||
pub const max_textures = 4096;
|
||
|
||
pub fn init(engine: *Engine, allocator: std.mem.Allocator) !Textures {
|
||
var map: std.AutoHashMapUnmanaged(Key, Id) = .empty;
|
||
errdefer map.deinit(allocator);
|
||
try map.ensureTotalCapacity(allocator, max_textures);
|
||
|
||
var array: std.ArrayList(Texture) = try .initCapacity(allocator, max_textures);
|
||
errdefer {
|
||
for (array.items) |*texture| {
|
||
texture.deinit(engine);
|
||
}
|
||
array.deinit(allocator);
|
||
}
|
||
|
||
// VOLATILE Synchronize with explicit values on top of `Id` type.
|
||
|
||
const empty_base_color_texture = try Texture.init(engine, .{
|
||
.width = 1,
|
||
.height = 1,
|
||
.usage = .base_color,
|
||
.target_queue = .graphics,
|
||
.name = "@Empty",
|
||
});
|
||
array.appendAssumeCapacity(empty_base_color_texture);
|
||
|
||
const empty_emissive_texture = try Texture.init(engine, .{
|
||
.width = 1,
|
||
.height = 1,
|
||
.usage = .emissive,
|
||
.target_queue = .graphics,
|
||
.name = "@Empty",
|
||
});
|
||
array.appendAssumeCapacity(empty_emissive_texture);
|
||
|
||
const empty_normal_texture = try Texture.init(engine, .{
|
||
.width = 1,
|
||
.height = 1,
|
||
.usage = .normal,
|
||
.target_queue = .graphics,
|
||
.name = "@Empty",
|
||
});
|
||
array.appendAssumeCapacity(empty_normal_texture);
|
||
|
||
const empty_occlusuion_roughness_metallic_texture = try Texture.init(engine, .{
|
||
.width = 1,
|
||
.height = 1,
|
||
.usage = .occlusion_roughness_metallic,
|
||
.target_queue = .graphics,
|
||
.name = "@Empty",
|
||
});
|
||
array.appendAssumeCapacity(empty_occlusuion_roughness_metallic_texture);
|
||
|
||
try empty_base_color_texture.writeSamples(u8, engine, &.{ 255, 255, 255, 255 });
|
||
try empty_emissive_texture.writeSamples(f16, engine, &.{ 1.0, 1.0, 1.0, 1.0 });
|
||
try empty_normal_texture.writeSamples(i8, engine, &.{ 0, 0, 127, 127 });
|
||
try empty_occlusuion_roughness_metallic_texture.writeSamples(u8, engine, &.{ 255, 255, 255, 255 });
|
||
|
||
return .{
|
||
.map = map,
|
||
.array = array,
|
||
};
|
||
}
|
||
|
||
pub fn deinit(self: *Textures, engine: *Engine, allocator: std.mem.Allocator) void {
|
||
std.log.scoped(.deinit).debug("Deinitializing {*} with {*} and Allocator{{{*},{*}}}", .{ self, engine, allocator.ptr, allocator.vtable });
|
||
|
||
for (self.array.items) |*texture| {
|
||
texture.deinit(engine);
|
||
}
|
||
self.array.deinit(allocator);
|
||
self.map.deinit(allocator);
|
||
self.* = undefined;
|
||
}
|
||
|
||
/// Get the ID of a texture given its filename (as a string) and usage. Returns
|
||
/// `null` if such texture hasn't been loaded. When the filename is `null`,
|
||
/// returns an empty texture ID appropriate for given usage.
|
||
pub fn get(self: *const Textures, maybe_filename: ?[]const u8, usage: Texture.Usage) ?Id {
|
||
if (maybe_filename) |filename| {
|
||
return self.map.get(.{
|
||
// If the atom doesn't exist, then the texture cannot possibly exist.
|
||
.filename = .fromStringIfExists(filename) orelse return null,
|
||
.usage = usage,
|
||
});
|
||
} else {
|
||
return emptyTextureForUsage(usage);
|
||
}
|
||
}
|
||
|
||
/// Get the ID of a texture given its filename (as an atom) and usage. Returns
|
||
/// `null` if such texture hasn't been loaded. When the filename is `.empty`,
|
||
/// returns an empty texture ID appropriate for given usage.
|
||
pub fn getAtom(self: *const Textures, filename: Atom, usage: Texture.Usage) ?Id {
|
||
if (filename != .empty) {
|
||
return self.map.get(.{
|
||
.filename = filename,
|
||
.usage = usage,
|
||
});
|
||
} else {
|
||
return emptyTextureForUsage(usage);
|
||
}
|
||
}
|
||
|
||
/// Get the ID of a texture given its filename (as a string) and usage. Returns
|
||
/// either an existing texture ID or loads a new texture and assigns a new ID,
|
||
/// if necessary. Will not return any error if the texture already exists. When
|
||
/// the filename is `null`, returns an empty texture ID appropriate for given
|
||
/// usage.
|
||
pub fn getOrLoad(
|
||
self: *Textures,
|
||
engine: *Engine,
|
||
stbi: *media.stbi,
|
||
maybe_filename: ?[]const u8,
|
||
usage: Texture.Usage,
|
||
io: std.Io,
|
||
) !Id {
|
||
if (maybe_filename) |filename| {
|
||
const key: Key = .{
|
||
// If the texture already exists, then the atom must exist and the
|
||
// following line will not return any error.
|
||
.filename = try .fromString(filename, io),
|
||
.usage = usage,
|
||
};
|
||
|
||
// We don't use `getOrPutAssumeCapacity` method, because we might already be
|
||
// at full capacity, in which case we should return `error.OutOfTextures`.
|
||
|
||
if (self.map.get(key)) |id| {
|
||
return id;
|
||
} else {
|
||
const id = Id.fromIndexSafe(self.array.items.len) catch |err| switch (err) {
|
||
error.Overflow => return error.OutOfTextures,
|
||
};
|
||
const texture = try loadTexture(engine, stbi, filename, usage, io);
|
||
|
||
self.map.putAssumeCapacityNoClobber(key, id);
|
||
self.array.appendAssumeCapacity(texture);
|
||
|
||
return id;
|
||
}
|
||
} else {
|
||
return emptyTextureForUsage(usage);
|
||
}
|
||
}
|
||
|
||
/// Get the ID of a texture given its filename (as an atom) and usage. Returns
|
||
/// either an existing texture ID or loads a new texture and assigns a new ID,
|
||
/// if necessary. Will not return any error if the texture already exists. When
|
||
/// the filename is `.empty`, returns an empty texture ID appropriate for given
|
||
/// usage.
|
||
pub fn getOrLoadAtom(
|
||
self: *Textures,
|
||
engine: *Engine,
|
||
stbi: *media.stbi,
|
||
filename: Atom,
|
||
usage: Texture.Usage,
|
||
io: std.Io,
|
||
) !Id {
|
||
if (filename != .empty) {
|
||
const key: Key = .{
|
||
.filename = filename,
|
||
.usage = usage,
|
||
};
|
||
|
||
// We don't use `getOrPutAssumeCapacity` method, because we might already be
|
||
// at full capacity, in which case we should return `error.OutOfTextures`.
|
||
|
||
if (self.map.get(key)) |id| {
|
||
return id;
|
||
} else {
|
||
const id = Id.fromIndexSafe(self.array.items.len) catch |err| switch (err) {
|
||
error.Overflow => return error.OutOfTextures,
|
||
};
|
||
const texture = try loadTexture(engine, stbi, filename.toString(), usage, io);
|
||
|
||
self.map.putAssumeCapacityNoClobber(key, id);
|
||
self.array.appendAssumeCapacity(texture);
|
||
|
||
return id;
|
||
}
|
||
} else {
|
||
return emptyTextureForUsage(usage);
|
||
}
|
||
}
|
||
|
||
fn loadTexture(
|
||
engine: *Engine,
|
||
stbi: *media.stbi,
|
||
filename: []const u8,
|
||
usage: Texture.Usage,
|
||
io: std.Io,
|
||
) !Texture {
|
||
std.log.debug("Loading texture \"{s}\" as {s}...", .{ filename, @tagName(usage) });
|
||
|
||
const cwd = std.Io.Dir.cwd();
|
||
|
||
var dir = try cwd.openDir(io, "assets/textures", .{});
|
||
defer dir.close(io);
|
||
|
||
var file = try dir.openFile(io, filename, .{});
|
||
defer file.close(io);
|
||
|
||
// The textures are expected to be small; a standard block base color as a
|
||
// PNG takes well below 1 kiB.
|
||
var buf: [4096]u8 = undefined;
|
||
var reader = file.reader(io, &buf);
|
||
|
||
const img = try stbi.loadDynamicIo(&reader.interface);
|
||
defer stbi.freeDynamic(img);
|
||
|
||
const data = img.data[0 .. img.width * img.height];
|
||
|
||
if (usage == .normal) {
|
||
for (data) |*pixel| {
|
||
pixel.r = pixel.r +% 128;
|
||
pixel.g = pixel.g +% 128;
|
||
pixel.b = pixel.b +% 128;
|
||
pixel.a = pixel.a +% 128;
|
||
}
|
||
}
|
||
|
||
var texture = try Texture.init(engine, .{
|
||
.width = img.width,
|
||
.height = img.height,
|
||
.usage = usage,
|
||
.target_queue = .graphics,
|
||
.name = filename,
|
||
});
|
||
errdefer texture.deinit(engine);
|
||
|
||
try texture.writeRaw(engine, @ptrCast(data));
|
||
return texture;
|
||
}
|
||
|
||
fn emptyTextureForUsage(usage: Texture.Usage) !Id {
|
||
return switch (usage) {
|
||
.base_color => .empty_base_color,
|
||
.normal => .empty_normal,
|
||
.occlusion_roughness_metallic => .empty_occlusion_roughness_metallic,
|
||
.emissive => .empty_emissive,
|
||
.depth => error.InvalidUsage,
|
||
};
|
||
}
|