More refactors around assets. Trust me, we need them
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
//! Module for loading persistent textures.
|
||||
|
||||
const Textures = @This();
|
||||
const std = @import("std");
|
||||
|
||||
@@ -7,44 +9,77 @@ const Atom = @import("../engine/Atom.zig").Atom;
|
||||
const Engine = @import("../engine/Engine.zig");
|
||||
const Texture = @import("../engine/Texture.zig");
|
||||
|
||||
map: Map,
|
||||
textures: Array,
|
||||
|
||||
pub const capacity = std.math.maxInt(std.meta.Tag(Id));
|
||||
|
||||
pub const Key = struct {
|
||||
atom: Atom,
|
||||
usage: Texture.Usage,
|
||||
};
|
||||
|
||||
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,
|
||||
_,
|
||||
|
||||
pub fn next(self: Id) Id {
|
||||
return @enumFromInt(@intFromEnum(self) + 1);
|
||||
/// 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 Map = std.AutoHashMapUnmanaged(Key, Id);
|
||||
pub const Array = std.ArrayList(Texture);
|
||||
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: Map = .empty;
|
||||
var map: std.AutoHashMapUnmanaged(Key, Id) = .empty;
|
||||
errdefer map.deinit(allocator);
|
||||
try map.ensureTotalCapacity(allocator, capacity);
|
||||
try map.ensureTotalCapacity(allocator, max_textures);
|
||||
|
||||
var textures: Array = try .initCapacity(allocator, capacity);
|
||||
var array: std.ArrayList(Texture) = try .initCapacity(allocator, max_textures);
|
||||
errdefer {
|
||||
for (textures.items) |*texture| {
|
||||
for (array.items) |*texture| {
|
||||
texture.deinit(engine);
|
||||
}
|
||||
textures.deinit(allocator);
|
||||
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,
|
||||
@@ -52,7 +87,7 @@ pub fn init(engine: *Engine, allocator: std.mem.Allocator) !Textures {
|
||||
.target_queue = .graphics,
|
||||
.name = "@Empty",
|
||||
});
|
||||
textures.appendAssumeCapacity(empty_base_color_texture);
|
||||
array.appendAssumeCapacity(empty_base_color_texture);
|
||||
|
||||
const empty_emissive_texture = try Texture.init(engine, .{
|
||||
.width = 1,
|
||||
@@ -61,7 +96,7 @@ pub fn init(engine: *Engine, allocator: std.mem.Allocator) !Textures {
|
||||
.target_queue = .graphics,
|
||||
.name = "@Empty",
|
||||
});
|
||||
textures.appendAssumeCapacity(empty_emissive_texture);
|
||||
array.appendAssumeCapacity(empty_emissive_texture);
|
||||
|
||||
const empty_normal_texture = try Texture.init(engine, .{
|
||||
.width = 1,
|
||||
@@ -70,7 +105,7 @@ pub fn init(engine: *Engine, allocator: std.mem.Allocator) !Textures {
|
||||
.target_queue = .graphics,
|
||||
.name = "@Empty",
|
||||
});
|
||||
textures.appendAssumeCapacity(empty_normal_texture);
|
||||
array.appendAssumeCapacity(empty_normal_texture);
|
||||
|
||||
const empty_occlusuion_roughness_metallic_texture = try Texture.init(engine, .{
|
||||
.width = 1,
|
||||
@@ -79,7 +114,7 @@ pub fn init(engine: *Engine, allocator: std.mem.Allocator) !Textures {
|
||||
.target_queue = .graphics,
|
||||
.name = "@Empty",
|
||||
});
|
||||
textures.appendAssumeCapacity(empty_occlusuion_roughness_metallic_texture);
|
||||
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 });
|
||||
@@ -88,67 +123,129 @@ pub fn init(engine: *Engine, allocator: std.mem.Allocator) !Textures {
|
||||
|
||||
return .{
|
||||
.map = map,
|
||||
.textures = textures,
|
||||
.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.textures.items) |*texture| {
|
||||
for (self.array.items) |*texture| {
|
||||
texture.deinit(engine);
|
||||
}
|
||||
self.textures.deinit(allocator);
|
||||
self.array.deinit(allocator);
|
||||
self.map.deinit(allocator);
|
||||
self.* = undefined;
|
||||
}
|
||||
|
||||
pub fn getAtom(self: *const Textures, atom: Atom, usage: Texture.Usage) ?Id {
|
||||
const key: Key = .{ .atom = atom, .usage = usage };
|
||||
return self.map.get(key);
|
||||
}
|
||||
|
||||
pub fn getFilename(self: *const Textures, filename: []const u8, usage: Texture.Usage) ?Id {
|
||||
const atom = Atom.fromStringIfExists(filename) orelse return null;
|
||||
const key: Key = .{ .atom = atom, .usage = usage };
|
||||
return self.map.get(key);
|
||||
}
|
||||
|
||||
pub fn getTexture(self: *const Textures, id: Id) ?*Texture {
|
||||
const index: usize = @intFromEnum(id);
|
||||
return if (index < self.textures.items.len) &self.textures.items[index] else null;
|
||||
}
|
||||
|
||||
pub fn getOrLoadAtom(self: *Textures, engine: *Engine, atom: Atom, usage: Texture.Usage, temp_allocator: std.mem.Allocator) !Id {
|
||||
const key: Key = .{ .atom = atom, .usage = usage };
|
||||
const entry = self.map.getOrPutAssumeCapacity(key);
|
||||
|
||||
if (entry.found_existing) {
|
||||
return entry.value_ptr.*;
|
||||
/// 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 {
|
||||
errdefer _ = self.map.remove(key);
|
||||
const texture = try loadTexture(engine, atom.toString(), usage, temp_allocator);
|
||||
const id = self.nextId();
|
||||
entry.value_ptr.* = id;
|
||||
self.textures.appendAssumeCapacity(texture);
|
||||
return id;
|
||||
return emptyTextureForUsage(usage);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getOrLoadFilename(self: *Textures, engine: *Engine, filename: []const u8, usage: Texture.Usage, temp_allocator: std.mem.Allocator) !Id {
|
||||
const atom = try Atom.fromString(filename);
|
||||
const key: Key = .{ .atom = atom, .usage = usage };
|
||||
const entry = self.map.getOrPutAssumeCapacity(key);
|
||||
|
||||
if (entry.found_existing) {
|
||||
return entry.value_ptr.*;
|
||||
/// 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 {
|
||||
errdefer _ = self.map.remove(key);
|
||||
const texture = try loadTexture(engine, filename, usage, temp_allocator);
|
||||
const id = self.nextId();
|
||||
entry.value_ptr.* = id;
|
||||
self.textures.appendAssumeCapacity(texture);
|
||||
return id;
|
||||
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.
|
||||
///
|
||||
/// When a texture is being loaded, `temp_allocator` is used for temporary
|
||||
/// allocations necessary to perform all operations. No memory allocated with
|
||||
/// `temp_allocator` is retained, so the allocator can be deinitialized or reset
|
||||
/// after this function returns. Note that during loading the engine will make
|
||||
/// its own persistent allocations, so an out of memory error is not necessarily
|
||||
/// related to `temp_allocator`.
|
||||
pub fn getOrLoad(self: *Textures, engine: *Engine, maybe_filename: ?[]const u8, usage: Texture.Usage, temp_allocator: std.mem.Allocator) !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),
|
||||
.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, filename, usage, temp_allocator);
|
||||
|
||||
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.
|
||||
///
|
||||
/// When a texture is being loaded, `temp_allocator` is used for temporary
|
||||
/// allocations necessary to perform all operations. No memory allocated with
|
||||
/// `temp_allocator` is retained, so the allocator can be deinitialized or reset
|
||||
/// after this function returns. Note that during loading the engine will make
|
||||
/// its own persistent allocations, so an out of memory is not necessarily
|
||||
/// related to `temp_allocator`.
|
||||
pub fn getOrLoadAtom(self: *Textures, engine: *Engine, filename: Atom, usage: Texture.Usage, temp_allocator: std.mem.Allocator) !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, filename.toString(), usage, temp_allocator);
|
||||
|
||||
self.map.putAssumeCapacityNoClobber(key, id);
|
||||
self.array.appendAssumeCapacity(texture);
|
||||
|
||||
return id;
|
||||
}
|
||||
} else {
|
||||
return emptyTextureForUsage(usage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,10 +257,7 @@ fn loadTexture(engine: *Engine, filename: []const u8, usage: Texture.Usage, temp
|
||||
var dir = try cwd.openDir("assets/textures", .{});
|
||||
defer dir.close();
|
||||
|
||||
const file = try dir.openFile(filename, .{});
|
||||
defer file.close();
|
||||
|
||||
const file_buf = try file.readToEndAlloc(temp_allocator, std.math.maxInt(usize));
|
||||
const file_buf = try dir.readFileAlloc(temp_allocator, filename, std.math.maxInt(usize));
|
||||
defer temp_allocator.free(file_buf);
|
||||
|
||||
var img = try stbi.Image.loadFromMemory(file_buf, usage.samplesPerTexel());
|
||||
@@ -188,7 +282,12 @@ fn loadTexture(engine: *Engine, filename: []const u8, usage: Texture.Usage, temp
|
||||
return texture;
|
||||
}
|
||||
|
||||
fn nextId(self: *const Textures) Id {
|
||||
const index = self.textures.items.len;
|
||||
return @enumFromInt(index);
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user