engine/src/AssetManager.zig

1683 lines
52 KiB
Zig

// TODO:
// - Don't allocate asset ids dynamically
// - Store asset memory usage on CPU and GPU
// - Use LRU to evict unused assets based on available memory
// (if we have enough memory never free, lol)
//
// NOTE: 1
// Renderer/Game code will touch assets each time they are used
// so LRU should work pretty well I think and I don't have to retain asset ids
// since they'll be pre-generated constants.
//
// NOTE: 2
// It makes hot reloading easier because it eliminates load*() calls completely
// Because each time an asset is used it's touched by using code, hot reload in asset
// server only needs to free assets and not actually reload them, cause they'll be reloaded
// next time they're used
const std = @import("std");
const gl = @import("gl.zig");
const fs_utils = @import("fs/utils.zig");
const formats = @import("formats.zig");
const asset_manifest = @import("asset_manifest");
const assets = @import("assets");
const checkGLError = @import("Render.zig").checkGLError;
const BuddyAllocator = @import("BuddyAllocator.zig");
const Vec2 = @import("zalgebra").Vec2;
const Vec3 = @import("zalgebra").Vec3;
const Mat4 = @import("zalgebra").Mat4;
const sdl = @import("sdl.zig");
const tracy = @import("tracy");
pub const AssetId = assets.AssetId;
pub const Handle = assets.Handle;
pub const AssetManager = @This();
const AssetIdList = std.SegmentedList(AssetId, 4);
const PowerOfTwo = u16;
const SHADER_MAX_BYTES = 1024 * 1024 * 50;
const MESH_MAX_BYTES = 1024 * 1024 * 500;
const TEXTURE_MAX_BYTES = 1024 * 1024 * 500;
allocator: std.mem.Allocator,
frame_arena: std.mem.Allocator,
// All assets are relative to exe dir
exe_dir: std.fs.Dir,
modified_times: std.AutoHashMapUnmanaged(AssetId, i128) = .{},
// Mapping from asset to all assets it depends on
dependencies: std.AutoHashMapUnmanaged(AssetId, std.SegmentedList(AssetId, 4)) = .{},
// Mapping from asset to all assets that depend on it
dependees: std.AutoHashMapUnmanaged(AssetId, std.SegmentedList(AssetId, 4)) = .{},
loaded_assets: std.AutoHashMapUnmanaged(AssetId, LoadedAsset) = .{},
rw_lock: std.Thread.RwLock.DefaultRwLock = .{},
asset_watcher: AssetWatcher = undefined,
vertex_heap: VertexBufferHeap,
const AssetWatcher = struct {
assetman: *AssetManager,
fba: std.heap.FixedBufferAllocator,
thread: ?*sdl.SDL_Thread = null,
finished: std.atomic.Value(bool) = std.atomic.Value(bool).init(false),
const THREAD_MEMORY = 1024 * 1024 * 32;
pub fn init(assetman: *AssetManager) !AssetWatcher {
const memory_bytes = try assetman.allocator.alloc(u8, THREAD_MEMORY);
return AssetWatcher{
.assetman = assetman,
.fba = std.heap.FixedBufferAllocator.init(memory_bytes),
};
}
pub fn deinit(self: *AssetWatcher) void {
self.finished.store(true, .unordered);
if (self.thread) |thread| {
var status: c_int = 0;
sdl.SDL_WaitThread(thread, &status);
}
}
pub fn startWatching(self: *AssetWatcher) void {
self.thread = sdl.SDL_CreateThread(watcherThread, "AssetManager Watcher", @ptrCast(self)) orelse {
std.log.err("SDL Error: {s}\n", .{sdl.SDL_GetError()});
@panic("SDL_CreateThread");
};
}
fn watcherThread(userdata: ?*anyopaque) callconv(.C) c_int {
const self: *AssetWatcher = @alignCast(@ptrCast(userdata));
while (!self.finished.load(.unordered)) {
self.fba.reset();
self.watchChanges() catch |err| {
std.log.err("Watch Changes error: {}\n", .{err});
};
}
return 0;
}
// TODO: proper watching
fn watchChanges(self: *AssetWatcher) !void {
const zone = tracy.initZone(@src(), .{ .name = "AssetWatcher.watchChanges" });
defer zone.deinit();
var updatedList = std.SegmentedList(AssetId, 128){};
var modified_times: std.AutoHashMapUnmanaged(AssetId, i128) = .{};
{
self.assetman.rw_lock.lockShared();
defer self.assetman.rw_lock.unlockShared();
modified_times = try self.assetman.modified_times.clone(self.fba.allocator());
}
{
var iter = modified_times.iterator();
while (iter.next()) |entry| {
const modified_time = entry.value_ptr.*;
const asset_path = asset_manifest.getPath(entry.key_ptr.*);
// Might happen for shader permuted assets
if (asset_path.len == 0) continue;
if (self.assetman.didUpdate(asset_path, modified_time)) {
try updatedList.append(self.fba.allocator(), entry.key_ptr.*);
}
}
}
if (updatedList.len > 0) {
self.assetman.rw_lock.lock();
defer self.assetman.rw_lock.unlock();
var iter = updatedList.iterator(0);
while (iter.next()) |asset_id| {
self.assetman.unloadAssetWithDependees(asset_id.*);
}
}
}
};
pub fn init(allocator: std.mem.Allocator, frame_arena: std.mem.Allocator) AssetManager {
var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
const exe_dir_path = std.fs.selfExeDirPath(&buf) catch @panic("can't find self exe dir path");
const exe_dir = std.fs.openDirAbsolute(exe_dir_path, .{}) catch @panic("can't open self exe dir path");
return .{
.allocator = allocator,
.frame_arena = frame_arena,
.exe_dir = exe_dir,
.vertex_heap = VertexBufferHeap.init(allocator) catch @panic("OOM"),
};
}
pub fn initWatch(self: *AssetManager) void {
self.asset_watcher = AssetWatcher.init(self) catch @panic("AssetWatcher.init");
self.asset_watcher.startWatching();
}
pub fn deinit(self: *AssetManager) void {
self.asset_watcher.deinit();
var iter = self.loaded_assets.valueIterator();
while (iter.next()) |asset| {
self.freeAsset(asset);
}
self.modified_times.deinit(self.allocator);
self.dependees.deinit(self.allocator);
self.dependencies.deinit(self.allocator);
self.loaded_assets.deinit(self.allocator);
}
fn resolveAsset(self: *AssetManager, handle: AssetId) ?*LoadedAsset {
self.rw_lock.lockShared();
defer self.rw_lock.unlockShared();
return self.loaded_assets.getPtr(handle);
}
const DefinePair = struct {
key: []const u8,
value: []const u8,
};
fn permuteAssetIdDefines(id: AssetId, defines: []const DefinePair) AssetId {
var hash = std.hash.Wyhash.init(id);
hash.update(&std.mem.toBytes(id));
for (defines) |def| {
hash.update(def.key);
hash.update(def.value);
}
return hash.final();
}
pub fn resolveShaderWithDefines(self: *AssetManager, handle: Handle.Shader, defines: []const DefinePair) LoadedShader {
if (handle.id == 0) return NullShader;
const permuted_asset_id = permuteAssetIdDefines(handle.id, defines);
if (self.resolveAsset(permuted_asset_id)) |asset| {
return asset.shader;
}
return self.loadShader(handle.id, permuted_asset_id, defines);
}
pub fn resolveShaderProgram(self: *AssetManager, handle: Handle.ShaderProgram) LoadedShaderProgram {
return self.resolveShaderProgramWithDefines(handle, &.{});
}
pub fn resolveShaderProgramWithDefines(self: *AssetManager, handle: Handle.ShaderProgram, defines: []const DefinePair) LoadedShaderProgram {
if (handle.id == 0) return NullShaderProgram;
const permuted_asset_id = permuteAssetIdDefines(handle.id, defines);
if (self.resolveAsset(permuted_asset_id)) |asset| {
switch (asset.*) {
.shaderProgram => |shader| {
return shader;
},
else => unreachable,
}
}
return self.loadShaderProgram(handle, permuted_asset_id, defines);
}
pub fn resolveMesh(self: *AssetManager, handle: Handle.Mesh) LoadedMesh {
if (handle.id == 0) return NullMesh;
if (self.resolveAsset(handle.id)) |asset| {
switch (asset.*) {
.mesh => |mesh| {
return mesh;
},
else => unreachable,
}
}
return self.loadMesh(handle.id);
}
pub fn resolveTexture(self: *AssetManager, handle: Handle.Texture) LoadedTexture {
if (handle.id == 0) return NullTexture;
if (self.resolveAsset(handle.id)) |asset| {
switch (asset.*) {
.texture => |texture| {
return texture;
},
else => unreachable,
}
}
return self.loadTexture(handle.id);
}
pub fn resolveScene(self: *AssetManager, handle: Handle.Scene) formats.Scene {
if (handle.id == 0) return NullScene.scene;
if (self.resolveAsset(handle.id)) |asset| {
switch (asset.*) {
.scene => |scene| {
return scene.scene;
},
else => unreachable,
}
}
return self.loadScene(handle.id).scene;
}
pub fn resolveMaterial(self: *AssetManager, handle: Handle.Material) formats.Material {
if (handle.id == 0) return NullMaterial;
if (self.resolveAsset(handle.id)) |asset| {
switch (asset.*) {
.material => |material| {
return material;
},
else => unreachable,
}
}
return self.loadMaterial(handle.id);
}
fn didUpdate(self: *AssetManager, path: []const u8, last_modified: i128) bool {
const mod = fs_utils.getFileModifiedRelative(self.exe_dir, path) catch |err| {
std.log.err("ERROR: {}\nfailed to check file modtime {s}\n", .{ err, path });
return false;
};
return mod != last_modified;
}
pub const ShaderProgramDefinition = struct {
vertex: []const u8,
fragment: []const u8,
compute: []const u8,
};
pub fn loadShaderProgram(self: *AssetManager, handle: Handle.ShaderProgram, permuted_id: AssetId, defines: []const DefinePair) LoadedShaderProgram {
return self.loadShaderProgramErr(handle.id, permuted_id, defines) catch |err| {
std.log.err("Failed to load shader program {}\n", .{err});
return NullShaderProgram;
};
}
fn loadShaderProgramErr(self: *AssetManager, id: AssetId, permuted_id: AssetId, defines: []const DefinePair) !LoadedShaderProgram {
const data = try self.loadFile(self.frame_arena, asset_manifest.getPath(id), SHADER_MAX_BYTES);
const program = formats.ShaderProgram.fromBuffer(data.bytes);
const graphics_pipeline = program.flags.vertex and program.flags.fragment;
const compute_pipeline = program.flags.compute;
if (!graphics_pipeline and !compute_pipeline) {
std.log.err("Can't compile shader program {s} without vertex AND fragment shaders or a compute shader\n", .{asset_manifest.getPath(id)});
return error.UnsupportedShader;
}
// TODO: !!! this will keep shader source in memory as long as shader program is in memory
// probably don't want this!
const shader = self.resolveShaderWithDefines(program.shader, defines);
const prog = gl.createProgram();
errdefer gl.deleteProgram(prog);
if (program.flags.vertex and program.flags.fragment) {
const vertex_shader = try self.compileShader(shader.source, .vertex);
defer gl.deleteShader(vertex_shader);
const fragment_shader = try self.compileShader(shader.source, .fragment);
defer gl.deleteShader(fragment_shader);
gl.attachShader(prog, vertex_shader);
defer gl.detachShader(prog, vertex_shader);
gl.attachShader(prog, fragment_shader);
defer gl.detachShader(prog, fragment_shader);
gl.linkProgram(prog);
} else {
const compute_shader = try self.compileShader(shader.source, .compute);
defer gl.deleteShader(compute_shader);
gl.attachShader(prog, compute_shader);
defer gl.detachShader(prog, compute_shader);
gl.linkProgram(prog);
}
var success: c_int = 0;
gl.getProgramiv(prog, gl.LINK_STATUS, &success);
if (success == 0) {
var info_len: gl.GLint = 0;
gl.getProgramiv(prog, gl.INFO_LOG_LENGTH, &info_len);
if (info_len > 0) {
const info_log = try self.frame_arena.allocSentinel(u8, @intCast(info_len - 1), 0);
gl.getProgramInfoLog(prog, @intCast(info_log.len), null, info_log);
std.log.err("ERROR::PROGRAM::LINK_FAILED\n{s}\n", .{info_log});
} else {
std.log.err("ERROR::PROGRAM::LINK_FAILED\nNo info log.\n", .{});
}
return error.ProgramLinkFailed;
}
var program_length: gl.GLsizei = 0;
gl.getProgramiv(prog, gl.PROGRAM_BINARY_LENGTH, &program_length);
if (program_length > 0) {
const program_binary = try self.frame_arena.allocSentinel(u8, @intCast(program_length - 1), 0);
var binary_format: gl.GLenum = gl.NONE;
var return_len: gl.GLsizei = 0;
gl.getProgramBinary(prog, program_length, &return_len, &binary_format, @ptrCast(program_binary.ptr));
checkGLError();
if (program_length == return_len) {
std.log.debug("Program {s} binary:\n{s}\n", .{ asset_manifest.getPath(id), program_binary[0..@intCast(return_len)] });
}
}
const loaded_shader_program = LoadedShaderProgram{
.program = prog,
.permuted_id = permuted_id,
};
{
self.rw_lock.lock();
defer self.rw_lock.unlock();
try self.loaded_assets.put(self.allocator, permuted_id, .{
.shaderProgram = loaded_shader_program,
});
try self.modified_times.put(self.allocator, id, data.modified);
try self.addDependencies(permuted_id, &.{ id, shader.permuted_id });
}
return loaded_shader_program;
}
const NullShader = LoadedShader{
.source = "",
.permuted_id = 0,
};
const NullShaderProgram = LoadedShaderProgram{
.program = 0,
.permuted_id = 0,
};
const NullMesh = LoadedMesh{
.aabb = .{},
.heap_handle = .{},
.positions = BufferSlice{
.buffer = 0,
.offset = 0,
.stride = 0,
},
.normals = BufferSlice{
.buffer = 0,
.offset = 0,
.stride = 0,
},
.tangents = BufferSlice{
.buffer = 0,
.offset = 0,
.stride = 0,
},
.uvs = BufferSlice{
.buffer = 0,
.offset = 0,
.stride = 0,
},
.indices = IndexSlice{
.buffer = 0,
.offset = 0,
.count = 0,
.type = gl.UNSIGNED_SHORT,
.base_vertex = 0,
},
.material = .{},
};
const NullTexture = LoadedTexture{
.name = 0,
.handle = 0,
};
const NullScene = LoadedScene{
.buf = "",
.scene = .{},
};
const NullMaterial = formats.Material{};
pub fn loadMesh(self: *AssetManager, id: AssetId) LoadedMesh {
return self.loadMeshErr(id) catch |err| {
std.log.err("Error: {} loading mesh at path: {s}", .{ err, asset_manifest.getPath(id) });
return NullMesh;
};
}
fn loadMeshErr(self: *AssetManager, id: AssetId) !LoadedMesh {
const path = asset_manifest.getPath(id);
const data = try self.loadFile(self.frame_arena, path, MESH_MAX_BYTES);
defer self.frame_arena.free(data.bytes);
const mesh = formats.Mesh.fromBuffer(data.bytes);
const vertices_len = mesh.vertices.len;
const allocation = try self.vertex_heap.alloc(vertices_len, mesh.indices.len);
const vertex_offset = allocation.vertex.offset;
gl.namedBufferSubData(self.vertex_heap.vertices.buffer, @intCast(vertex_offset * @sizeOf(formats.Vector3)), @intCast(vertices_len * @sizeOf(formats.Vector3)), @ptrCast(mesh.vertices.ptr));
checkGLError();
gl.namedBufferSubData(self.vertex_heap.normals.buffer, @intCast(vertex_offset * @sizeOf(formats.Vector3)), @intCast(vertices_len * @sizeOf(formats.Vector3)), @ptrCast(mesh.normals.ptr));
checkGLError();
gl.namedBufferSubData(self.vertex_heap.tangents.buffer, @intCast(vertex_offset * @sizeOf(formats.Vector3)), @intCast(vertices_len * @sizeOf(formats.Vector3)), @ptrCast(mesh.tangents.ptr));
checkGLError();
gl.namedBufferSubData(self.vertex_heap.uvs.buffer, @intCast(vertex_offset * @sizeOf(formats.Vector2)), @intCast(vertices_len * @sizeOf(formats.Vector2)), @ptrCast(mesh.uvs.ptr));
checkGLError();
const index_offset = allocation.index.offset;
gl.namedBufferSubData(self.vertex_heap.indices.buffer, @intCast(index_offset * @sizeOf(formats.Index)), @intCast(mesh.indices.len * @sizeOf(formats.Index)), @ptrCast(mesh.indices.ptr));
const loaded_mesh = LoadedMesh{
.aabb = .{
.min = Vec3.new(mesh.aabb.min.x, mesh.aabb.min.y, mesh.aabb.min.z),
.max = Vec3.new(mesh.aabb.max.x, mesh.aabb.max.y, mesh.aabb.max.z),
},
.heap_handle = allocation,
.material = mesh.material,
.positions = .{
.buffer = self.vertex_heap.vertices.buffer,
.offset = @intCast(vertex_offset * @sizeOf(formats.Vector3)),
.stride = @sizeOf(formats.Vector3),
},
.normals = .{
.buffer = self.vertex_heap.normals.buffer,
.offset = @intCast(vertex_offset * @sizeOf(formats.Vector3)),
.stride = @sizeOf(formats.Vector3),
},
.tangents = .{
.buffer = self.vertex_heap.tangents.buffer,
.offset = @intCast(vertex_offset * @sizeOf(formats.Vector3)),
.stride = @sizeOf(formats.Vector3),
},
.uvs = .{
.buffer = self.vertex_heap.uvs.buffer,
.offset = @intCast(vertex_offset * @sizeOf(formats.Vector2)),
.stride = @sizeOf(formats.Vector2),
},
.indices = .{
.buffer = self.vertex_heap.indices.buffer,
.offset = @intCast(index_offset * @sizeOf(formats.Index)),
.count = @intCast(mesh.indices.len),
.type = gl.UNSIGNED_INT,
.base_vertex = @intCast(vertex_offset),
},
};
{
self.rw_lock.lock();
defer self.rw_lock.unlock();
try self.loaded_assets.put(self.allocator, id, .{ .mesh = loaded_mesh });
try self.modified_times.put(self.allocator, id, data.modified);
}
return loaded_mesh;
}
fn loadTexture(self: *AssetManager, id: AssetId) LoadedTexture {
return self.loadTextureErr(id) catch |err| {
std.log.err("Error: {} loading texture at path {s}\n", .{ err, asset_manifest.getPath(id) });
return NullTexture;
};
}
fn loadTextureErr(self: *AssetManager, id: AssetId) !LoadedTexture {
const path = asset_manifest.getPath(id);
const data = try self.loadFile(self.frame_arena, path, TEXTURE_MAX_BYTES);
defer self.frame_arena.free(data.bytes);
var texture = try formats.Texture.fromBuffer(self.allocator, data.bytes);
defer texture.free(self.allocator);
var name: gl.GLuint = 0;
gl.createTextures(gl.TEXTURE_2D, 1, &name);
if (name == 0) {
return error.GLCreateTexture;
}
errdefer gl.deleteTextures(1, &name);
const gl_format: gl.GLenum = switch (texture.header.format) {
.bc7 => gl.COMPRESSED_RGBA_BPTC_UNORM,
.bc5 => gl.COMPRESSED_RG_RGTC2,
.bc6 => gl.COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT,
};
gl.textureStorage2D(
name,
@intCast(texture.mipLevels()),
gl_format,
@intCast(texture.header.padded_width),
@intCast(texture.header.padded_height),
);
checkGLError();
for (0..texture.mipLevels()) |mip_level| {
const desc = texture.getMipDesc(mip_level);
gl.compressedTextureSubImage2D(
name,
@intCast(mip_level),
0,
0,
@intCast(desc.width),
@intCast(desc.height),
gl_format,
@intCast(texture.data[mip_level].len),
@ptrCast(texture.data[mip_level].ptr),
);
checkGLError();
}
const uv_scale = Vec2.new(
@as(f32, @floatFromInt(texture.header.width)) / @as(f32, @floatFromInt(texture.header.padded_width)),
@as(f32, @floatFromInt(texture.header.height)) / @as(f32, @floatFromInt(texture.header.padded_height)),
);
const handle = gl.GL_ARB_bindless_texture.getTextureHandleARB(name);
gl.GL_ARB_bindless_texture.makeTextureHandleResidentARB(handle);
errdefer gl.GL_ARB_bindless_texture.makeTextureHandleNonResidentARB(handle);
const loaded_texture = LoadedTexture{
.name = name,
.handle = handle,
.uv_scale = uv_scale,
};
{
self.rw_lock.lock();
defer self.rw_lock.unlock();
try self.loaded_assets.put(
self.allocator,
id,
.{ .texture = loaded_texture },
);
try self.modified_times.put(self.allocator, id, data.modified);
}
return loaded_texture;
}
fn loadScene(self: *AssetManager, id: AssetId) LoadedScene {
return self.loadSceneErr(id) catch |err| {
std.log.err("Error: {} loading scene at path {s}\n", .{ err, asset_manifest.getPath(id) });
return NullScene;
};
}
fn loadSceneErr(self: *AssetManager, id: AssetId) !LoadedScene {
const path = asset_manifest.getPath(id);
const data = try self.loadFile(self.allocator, path, TEXTURE_MAX_BYTES);
const scene = try formats.Scene.fromBuffer(data.bytes);
const loaded_scene = LoadedScene{
.buf = data.bytes,
.scene = scene,
};
{
self.rw_lock.lock();
defer self.rw_lock.unlock();
try self.loaded_assets.put(
self.allocator,
id,
.{
.scene = loaded_scene,
},
);
try self.modified_times.put(self.allocator, id, data.modified);
}
return loaded_scene;
}
fn loadMaterial(self: *AssetManager, id: AssetId) formats.Material {
return self.loadMaterialErr(id) catch |err| {
std.log.err("Error: {} loading material at path {s}\n", .{ err, asset_manifest.getPath(id) });
return NullMaterial;
};
}
fn loadMaterialErr(self: *AssetManager, id: AssetId) !formats.Material {
const path = asset_manifest.getPath(id);
const data = try self.loadFile(self.frame_arena, path, TEXTURE_MAX_BYTES);
const material = formats.Material.fromBuffer(data.bytes);
{
self.rw_lock.lock();
defer self.rw_lock.unlock();
try self.loaded_assets.put(
self.allocator,
id,
.{
.material = material,
},
);
try self.modified_times.put(self.allocator, id, data.modified);
}
return material;
}
const LoadedAsset = union(enum) {
shader: LoadedShader,
shaderProgram: LoadedShaderProgram,
mesh: LoadedMesh,
texture: LoadedTexture,
scene: LoadedScene,
material: formats.Material,
};
const LoadedShader = struct {
source: []const u8,
permuted_id: AssetId,
};
const LoadedShaderProgram = struct {
program: gl.GLuint,
permuted_id: AssetId,
};
pub const LoadedMesh = struct {
aabb: AABB,
heap_handle: VertexBufferHeap.Alloc,
positions: BufferSlice,
normals: BufferSlice,
tangents: BufferSlice,
uvs: BufferSlice,
indices: IndexSlice,
material: formats.Material,
};
const LoadedTexture = struct {
name: gl.GLuint,
handle: gl.GLuint64,
uv_scale: Vec2 = Vec2.one(),
};
const LoadedScene = struct {
// Buffer that holds scene data
buf: []const u8,
scene: formats.Scene,
};
pub const AABB = struct {
min: Vec3 = Vec3.zero(),
max: Vec3 = Vec3.zero(),
pub fn distance(self: *const AABB, point: Vec3) f32 {
const center = self.min.add(self.max).scale(0.5);
const extent = self.max.sub(self.min).scale(0.5);
var center_to_point = point.sub(center);
center_to_point.data = @abs(center_to_point.data);
var d = center_to_point.sub(extent);
d.data = @max(d.data, @as(@Vector(3, f32), @splat(0.0)));
const sq_dist_to_side = d.dot(d);
if (std.math.approxEqAbs(f32, sq_dist_to_side, 0.0, 0.0001)) {
const diff = point.sub(center);
return diff.dot(diff);
}
return sq_dist_to_side;
}
pub fn transformBy(self: *const AABB, matrix: Mat4) AABB {
var center = self.min.add(self.max).scale(0.5).toVec4(1.0);
var extent = self.max.sub(self.min).scale(0.5).toVec4(0.0);
center = matrix.mulByVec4(center);
extent = matrix.mulByVec4(extent);
return AABB{ .min = center.sub(extent).toVec3(), .max = center.add(extent).toVec3() };
}
};
pub const BufferSlice = struct {
buffer: gl.GLuint,
offset: gl.GLintptr,
stride: gl.GLsizei,
pub fn bind(self: *const BufferSlice, index: gl.GLuint) void {
gl.bindVertexBuffer(index, self.buffer, 0, self.stride);
}
};
pub const IndexSlice = struct {
buffer: gl.GLuint,
offset: gl.GLuint,
count: gl.GLuint,
type: gl.GLenum,
base_vertex: gl.GLint,
pub fn bind(self: *const IndexSlice) void {
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, self.buffer);
}
};
pub const ShaderType = enum {
vertex,
fragment,
compute,
pub fn goGLType(self: ShaderType) gl.GLenum {
return switch (self) {
.vertex => gl.VERTEX_SHADER,
.fragment => gl.FRAGMENT_SHADER,
.compute => gl.COMPUTE_SHADER,
};
}
const VERTEX_DEFINES = "#version 460 core\n#define VERTEX_SHADER 1\n#define VERTEX_EXPORT out\n";
const FRAGMENT_DEFINES = "#version 460 core\n#define FRAGMENT_SHADER 1\n#define VERTEX_EXPORT in\n";
const COMPUTE_DEFINES = "#version 460 core\n#define COMPUTE_SHADER 1\n";
pub fn getDefines(self: ShaderType) []const u8 {
return switch (self) {
.vertex => VERTEX_DEFINES,
.fragment => FRAGMENT_DEFINES,
.compute => COMPUTE_DEFINES,
};
}
};
const AssetData = struct {
bytes: []u8,
modified: i128,
};
fn loadFile(self: *AssetManager, allocator: std.mem.Allocator, path: []const u8, max_size: usize) !AssetData {
const file = try self.exe_dir.openFile(path, .{});
defer file.close();
const meta = try file.metadata();
const bytes = try file.reader().readAllAlloc(allocator, max_size);
return .{ .bytes = bytes, .modified = meta.modified() };
}
fn loadShader(self: *AssetManager, id: AssetId, permuted_id: AssetId, defines: []const DefinePair) LoadedShader {
return self.loadShaderErr(id, permuted_id, defines) catch |err| {
std.log.err("Error: {} when loading shader id {} {s}", .{ err, id, asset_manifest.getPath(id) });
return NullShader;
};
}
const ShaderTokenizer = struct {
const Self = @This();
pub const TokenType = enum {
Unknown,
OpenParen,
OpenBrace,
OpenBracket,
ClosedParen,
ClosedBrace,
ClosedBracket,
Comma,
Colon,
Semicolon,
Question,
Tilde,
Dot,
Star,
Plus,
Dash,
Slash,
Percent,
Caret,
Bar,
Ampersand,
StarEquals,
PlusEquals,
DashEquals,
SlashEquals,
PercentEquals,
CaretEquals,
BarEquals,
AmpersandEquals,
DoubleBar,
DoubleAmpersand,
Equals,
EqualsEquals,
Bang,
BangEquals,
Greater,
GreaterGreater, // >>
Less,
LessLess, // <<
GreaterEquals,
LessEquals,
String,
Directive,
Number,
Identifier,
End,
};
pub const Token = struct {
type: TokenType = .Unknown,
text: []const u8 = "",
// Start and end of the token including things like quotes
start: usize = 0,
end: usize = 0,
};
at: usize = 0,
data: []const u8,
pub fn init(data: []const u8) Self {
return Self{ .data = data };
}
fn peek(self: *const Self) ?u8 {
if (self.at + 1 < self.data.len) {
return self.data[self.at + 1];
}
return null;
}
fn matchDoubleSymbol(self: *Self, token: *Token, next_sym: u8, no_match: TokenType, match: TokenType) bool {
if (self.peek()) |next_char| {
if (next_char == next_sym) {
token.type = match;
token.end += 1;
token.text = self.data[token.start..token.end];
return true;
} else {
token.type = no_match;
}
} else {
token.type = no_match;
}
return false;
}
fn isHex(c: u8) bool {
return switch (c) {
'0'...'9' => true,
'a'...'f', 'A'...'F' => true,
else => false,
};
}
fn matchInteger(self: *Self) ?Token {
var token = Token{ .type = .Number, .start = self.at };
var has_sign = false;
if (self.data[self.at] == '-') {
self.at += 1;
has_sign = true;
}
switch (self.data[self.at]) {
'0' => {
if (self.peek()) |next_sym| {
switch (next_sym) {
'x', 'X' => {
// HEX
self.at += 2;
while (isHex(self.data[self.at])) {
self.at += 1;
}
},
'0'...'9' => {
self.at += 1;
// Octal, maybe invalid (8, 9 are invalid)
while (isNum(self.data[self.at])) {
self.at += 1;
}
},
else => {
self.at += 1;
},
}
} else {
self.at += 1;
}
},
'1'...'9' => {
while (self.at < self.data.len and isNum(self.data[self.at])) {
self.at += 1;
}
},
else => {},
}
switch (self.data[self.at]) {
'u', 'U' => {
self.at += 1;
},
else => {},
}
token.end = self.at;
token.text = self.data[token.start..token.end];
const len_without_sign = if (has_sign) token.text.len - 1 else token.text.len;
if (len_without_sign == 0) {
return null;
}
return token;
}
fn eatDigitSequence(self: *Self) void {
while (self.at < self.data.len and isNum(self.data[self.at])) {
self.at += 1;
}
}
fn matchFloat(self: *Self) ?Token {
var token = Token{ .type = .Number, .start = self.at };
var has_sign = false;
switch (self.data[self.at]) {
'-', '+' => {
self.at += 1;
has_sign = true;
},
else => {},
}
self.eatDigitSequence();
if (self.data[self.at] == '.') {
self.at += 1;
}
self.eatDigitSequence();
// Exponent
if (self.data[self.at] == 'e' or self.data[self.at] == 'E') {
self.at += 1;
}
//Suffix
switch (self.data[self.at]) {
'f', 'F' => {
self.at += 1;
},
'l' => {
if (self.peek() == 'f') {
self.at += 2;
}
},
'L' => {
if (self.peek() == 'F') {
self.at += 2;
}
},
else => {},
}
token.end = self.at;
token.text = self.data[token.start..token.end];
const len_without_sign = if (has_sign) token.text.len - 1 else token.text.len;
if (len_without_sign == 0) {
return null;
}
return token;
}
fn matchNumber(self: *Self, token: *Token) bool {
const start = self.at;
const maybe_int_token = self.matchInteger();
self.at = start;
const maybe_float_token = self.matchFloat();
if (maybe_int_token != null and maybe_float_token != null) {
const int_token = maybe_int_token.?;
const float_token = maybe_float_token.?;
if (int_token.end > float_token.end) {
self.at = int_token.end;
token.* = int_token;
} else {
self.at = float_token.end;
token.* = float_token;
}
return true;
}
if (maybe_float_token) |result| {
self.at = result.end;
token.* = result;
return true;
}
if (maybe_int_token) |result| {
self.at = result.end;
token.* = result;
return true;
}
return false;
}
pub fn next(self: *Self) Token {
self.eatWhitespace();
if (self.at == self.data.len) {
return Token{ .type = .End };
}
var result = Token{ .type = .Unknown, .start = self.at, .end = self.at + 1, .text = self.data[self.at .. self.at + 1] };
if (self.at < self.data.len) {
switch (self.data[self.at]) {
'(' => {
result.type = .OpenParen;
},
'{' => {
result.type = .OpenBrace;
},
'[' => {
result.type = .OpenBracket;
},
')' => {
result.type = .ClosedParen;
},
'}' => {
result.type = .ClosedBrace;
},
']' => {
result.type = .ClosedBracket;
},
',' => {
result.type = .Comma;
},
':' => {
result.type = .Colon;
},
';' => {
result.type = .Semicolon;
},
'?' => {
result.type = .Question;
},
'~' => {
result.type = .Tilde;
},
'.' => {
if (!self.matchNumber(&result)) {
result.type = .Dot;
}
},
'*' => {
_ = self.matchDoubleSymbol(&result, '=', .Star, .StarEquals);
},
'+' => {
if (!self.matchNumber(&result)) {
_ = self.matchDoubleSymbol(&result, '=', .Plus, .PlusEquals);
}
},
'-' => {
if (!self.matchNumber(&result)) {
_ = self.matchDoubleSymbol(&result, '=', .Dash, .DashEquals);
}
},
'/' => {
_ = self.matchDoubleSymbol(&result, '=', .Slash, .SlashEquals);
},
'%' => {
_ = self.matchDoubleSymbol(&result, '=', .Percent, .PercentEquals);
},
'^' => {
_ = self.matchDoubleSymbol(&result, '=', .Caret, .StarEquals);
},
'|' => {
if (!self.matchDoubleSymbol(&result, '=', .Bar, .BarEquals)) {
_ = self.matchDoubleSymbol(&result, '|', .Bar, .DoubleBar);
}
},
'&' => {
if (!self.matchDoubleSymbol(&result, '=', .Ampersand, .AmpersandEquals)) {
_ = self.matchDoubleSymbol(&result, '&', .Ampersand, .DoubleAmpersand);
}
},
'=' => {
_ = self.matchDoubleSymbol(&result, '=', .Equals, .EqualsEquals);
},
'!' => {
_ = self.matchDoubleSymbol(&result, '=', .Bang, .BangEquals);
},
'<' => {
if (!self.matchDoubleSymbol(&result, '=', .Less, .LessEquals)) {
_ = self.matchDoubleSymbol(&result, '<', .Less, .LessLess);
}
},
'>' => {
if (!self.matchDoubleSymbol(&result, '=', .Greater, .GreaterEquals)) {
_ = self.matchDoubleSymbol(&result, '>', .Greater, .GreaterGreater);
}
},
'"' => {
const start = self.at;
self.at += 1;
const text_start = self.at;
while (self.at < self.data.len and self.data[self.at] != '"') {
if (self.data[self.at] == '\\') {
self.at += 1;
}
self.at += 1;
}
const text_end = self.at;
if (self.data[self.at] == '"') {
self.at += 1;
}
const end = self.at;
result.type = .String;
result.start = start;
result.text = self.data[text_start..text_end];
result.start = start;
result.end = end;
},
'#' => {
const start = self.at;
self.at += 1;
const text_start = self.at;
while (isAlphaNum(self.data[self.at])) {
self.at += 1;
}
const end = self.at;
result.type = .Directive;
result.text = self.data[text_start..end];
result.start = start;
result.end = end;
},
'a'...'z', 'A'...'Z', '_' => {
const start = self.at;
while (self.at < self.data.len and (isAlphaNum(self.data[self.at]))) {
self.at += 1;
}
const end = self.at;
result.type = .Identifier;
result.text = self.data[start..end];
result.start = start;
result.end = end;
},
'0'...'9' => {
const matched = self.matchNumber(&result);
std.debug.assert(matched);
},
else => {},
}
} else {
result.type = .End;
}
self.at = result.end;
return result;
}
fn isWhitespace(c: u8) bool {
return c == ' ' or c == '\t' or c == '\n' or c == '\r';
}
fn isAlpha(c: u8) bool {
return (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z');
}
fn isNum(c: u8) bool {
return (c >= '0' and c <= '9');
}
fn isAlphaNum(c: u8) bool {
return isAlpha(c) or isNum(c) or c == '_';
}
fn eatWhitespace(self: *Self) void {
var consuming = true;
while (consuming) {
if (self.at >= self.data.len) {
return;
}
if (isWhitespace(self.data[self.at])) {
self.at += 1;
} else if (self.data[self.at] == '/' and self.peek() == '/') {
self.at += 2;
while (self.at < self.data.len and self.data[self.at] != '\n') {
if (self.data[self.at] == '\\' and self.peek() == '\n') {
self.at += 1;
}
self.at += 1;
}
} else if (self.data[self.at] == '/' and self.peek() == '*') {
self.at += 2;
while (self.at < self.data.len and self.at < self.data.len and !(self.data[self.at] == '*' and self.peek() == '/')) {
self.at += 1;
}
if (self.at < self.data.len and self.data[self.at] == '*' and self.peek() == '/') {
self.at += 2;
}
} else {
consuming = false;
}
}
}
};
test "ShaderTokenizer" {
const testing = std.testing;
var tokenizer = ShaderTokenizer.init(
\\// UBOs asdkfljlka ajksfk\
\\
\\#include "../my_file\".glsl"
\\
\\layout(std140, binding = 0) uniform Matrices {
\\ mat4 projection;
\\ mat4 view;
\\};
\\
\\layout(location = 2) uniform vec3 color;
\\
\\// Input, output blocks
\\
\\#if VERTEX_SHADER
\\
\\layout(location = 0) in vec3 aPos;
\\
\\void main() {
\\ gl_Position = projection * view * vec4(aPos.xyz, 1.0);
\\}
\\#endif // VERTEX_SHADER
\\
\\#if FRAGMENT_SHADER
\\
\\out vec4 FragColor;
\\
\\void main() {
\\ FragColor += vec4(vec3(1.0), 1.0f);
\\ bool logic = (true && false || _asdfjkh) | (1u << -123U);
\\ 0xAf123FF
\\ -0x123
\\ -09872
\\ .1
\\ .12f
\\ 0.2LF
\\ +0
\\ -0f
\\}
\\
\\
\\#endif // FRAGMNET_SHADER
);
var token = tokenizer.next();
while (token.type != .End) : (token = tokenizer.next()) {
// try std.io.getStdErr().writer().print("{} \"{s}\"\n", .{ token.type, token.text });
try testing.expect(token.type != .Unknown);
}
}
fn loadShaderErr(self: *AssetManager, id: AssetId, permuted_id: AssetId, defines: []const DefinePair) !LoadedShader {
const path = asset_manifest.getPath(id);
const dir = std.fs.path.dirname(path) orelse @panic("No dir");
const data = try self.loadFile(self.frame_arena, path, SHADER_MAX_BYTES);
var included_asset_ids = std.ArrayList(AssetId).init(self.frame_arena);
var preprocessed_segments = std.SegmentedList([]const u8, 64){};
var final_len: usize = 0;
// Just append defines here, no need to manually preprocess
for (defines) |define| {
const define_str = try std.fmt.allocPrint(self.frame_arena, "#define {s} {s}\n", .{ define.key, define.value });
try preprocessed_segments.append(self.frame_arena, define_str);
final_len += define_str.len;
}
// Preprocess
{
var tokenizer = ShaderTokenizer.init(data.bytes);
var last_offset: usize = 0;
var token = tokenizer.next();
while (token.type != .End) : (token = tokenizer.next()) {
switch (token.type) {
.Directive => {
if (std.mem.eql(u8, token.text, "include")) {
// Append section of text up to this directive
try preprocessed_segments.append(self.frame_arena, data.bytes[last_offset..token.start]);
final_len += token.start - last_offset;
const include_path = tokenizer.next();
// Next section will start after this directive, this allows replacing #include with its content
last_offset = include_path.end;
if (include_path.type != .String) {
return error.InvalidInclude;
}
const included_file_path = try std.fs.path.resolve(self.frame_arena, &.{ dir, include_path.text });
const included_asset_id = assets.AssetPath.fromString(included_file_path).hash();
if (included_asset_id != 0) {
const included_shader = self.resolveShaderWithDefines(.{ .id = included_asset_id }, defines);
try included_asset_ids.append(included_shader.permuted_id);
try preprocessed_segments.append(self.frame_arena, included_shader.source);
final_len += included_shader.source.len;
}
}
},
else => {},
}
}
{
const remaining = data.bytes.len - last_offset;
if (remaining > 0) {
try preprocessed_segments.append(self.frame_arena, data.bytes[last_offset..data.bytes.len]);
final_len += remaining;
}
}
}
var result_source = try self.allocator.alloc(u8, final_len);
// Join source sections
{
var cursor: usize = 0;
var iter = preprocessed_segments.constIterator(0);
while (iter.next()) |slice| {
@memcpy(result_source[cursor .. cursor + slice.len], slice.*);
cursor += slice.len;
}
}
const loaded_shader = LoadedShader{ .source = result_source, .permuted_id = permuted_id };
{
self.rw_lock.lock();
defer self.rw_lock.unlock();
try self.loaded_assets.put(self.allocator, permuted_id, .{ .shader = loaded_shader });
try self.modified_times.put(self.allocator, id, data.modified);
try self.addDependencies(permuted_id, &.{id});
try self.addDependencies(permuted_id, included_asset_ids.items);
}
return loaded_shader;
}
fn compileShader(self: *AssetManager, source: []const u8, shader_type: ShaderType) !gl.GLuint {
const shader = gl.createShader(shader_type.goGLType());
errdefer gl.deleteShader(shader);
std.debug.assert(shader != 0); // should only happen if incorect shader type is passed
const defines = shader_type.getDefines();
gl.shaderSource(
shader,
2,
&[_][*c]const u8{ @ptrCast(shader_type.getDefines()), @ptrCast(source) },
&[_]gl.GLint{ @intCast(defines.len), @intCast(source.len) },
);
gl.compileShader(shader);
var success: c_int = 0;
gl.getShaderiv(shader, gl.COMPILE_STATUS, &success);
if (success == 0) {
var info_len: gl.GLint = 0;
gl.getShaderiv(shader, gl.INFO_LOG_LENGTH, &info_len);
if (info_len > 0) {
const info_log = try self.frame_arena.allocSentinel(u8, @intCast(info_len - 1), 0);
gl.getShaderInfoLog(shader, @intCast(info_log.len), null, info_log);
std.log.err("ERROR::SHADER::COMPILATION_FAILED\n{s}{s}\n{s}\n", .{ defines, source, info_log });
} else {
std.log.err("ERROR::SHADER::COMPILIATION_FAILED\n{s}{s}\nNo info log.\n", .{ defines, source });
}
return error.ShaderCompilationFailed;
}
return shader;
}
fn addDependencies(self: *AssetManager, id: AssetId, dependencies: []const AssetId) !void {
{
const gop = try self.dependencies.getOrPut(self.allocator, id);
if (!gop.found_existing) {
gop.value_ptr.* = .{};
}
try gop.value_ptr.appendSlice(self.allocator, dependencies);
}
for (dependencies) |dep| {
const gop = try self.dependees.getOrPut(self.allocator, dep);
if (!gop.found_existing) {
gop.value_ptr.* = .{};
}
try gop.value_ptr.append(self.allocator, id);
}
}
fn deleteDependees(self: *AssetManager, id: AssetId) void {
const dependees = self.dependees.getPtr(id) orelse return;
var iter = dependees.iterator(0);
while (iter.next()) |dep| {
self.unloadAssetWithDependees(dep.*);
}
}
fn freeAsset(self: *AssetManager, asset: *LoadedAsset) void {
switch (asset.*) {
.mesh => |*mesh| {
self.vertex_heap.free(mesh.heap_handle);
},
.shader => |*shader| {
self.allocator.free(shader.source);
},
.shaderProgram => |*program| {
gl.deleteProgram(program.program);
},
.texture => |*texture| {
gl.GL_ARB_bindless_texture.makeTextureHandleNonResidentARB(texture.handle);
gl.deleteTextures(1, &texture.name);
},
.scene => |*scene| {
self.allocator.free(scene.buf);
},
.material => {},
}
}
// Don't call without write lock
fn unloadAssetWithDependees(self: *AssetManager, id: AssetId) void {
std.log.debug("unload asset id {}: {s}\n", .{ id, asset_manifest.getPath(id) });
self.deleteDependees(id);
if (self.loaded_assets.getPtr(id)) |asset| {
self.freeAsset(asset);
}
_ = self.loaded_assets.remove(id);
_ = self.modified_times.remove(id);
_ = self.dependees.remove(id);
_ = self.dependencies.remove(id);
}
const VertexBufferHeap = struct {
const Self = @This();
pub const Alloc = struct {
vertex: BuddyAllocator.Alloc = .{},
index: BuddyAllocator.Alloc = .{},
};
pub const Buffer = struct {
buffer: gl.GLuint,
stride: gl.GLsizei,
pub fn init(name: gl.GLuint, stride: usize) Buffer {
return .{
.buffer = name,
.stride = @intCast(stride),
};
}
pub fn bind(self: *const Buffer, index: gl.GLuint) void {
gl.bindVertexBuffer(index, self.buffer, 0, self.stride);
}
};
vertex_buddy: BuddyAllocator,
index_buddy: BuddyAllocator,
vertices: Buffer,
normals: Buffer,
tangents: Buffer,
uvs: Buffer,
indices: Buffer,
pub fn init(allocator: std.mem.Allocator) !Self {
// 256 mega vertices :)
// memory usage for vertices (- indices) = n * 11 * 4
// 4096, 12 will take 704 mb for vertices
var vertex_buddy = try BuddyAllocator.init(allocator, 4096, 13);
errdefer vertex_buddy.deinit();
var index_buddy = try BuddyAllocator.init(allocator, 4096, 13);
errdefer index_buddy.deinit();
const vertex_buf_size = vertex_buddy.getSize();
const index_buf_size = index_buddy.getSize();
var bufs = [_]gl.GLuint{ 0, 0, 0, 0, 0 };
gl.createBuffers(bufs.len, &bufs);
errdefer gl.deleteBuffers(bufs.len, &bufs);
for (bufs) |buf| {
if (buf == 0) {
return error.BufferAllocationFailed;
}
}
const vertices = Buffer.init(bufs[0], @sizeOf(formats.Vector3));
const normals = Buffer.init(bufs[1], @sizeOf(formats.Vector3));
const tangents = Buffer.init(bufs[2], @sizeOf(formats.Vector3));
const uvs = Buffer.init(bufs[3], @sizeOf(formats.Vector2));
const indices = Buffer.init(bufs[4], @sizeOf(formats.Index));
gl.namedBufferStorage(
vertices.buffer,
@intCast(vertex_buf_size * @sizeOf(formats.Vector3)),
null,
gl.DYNAMIC_STORAGE_BIT,
);
gl.namedBufferStorage(
normals.buffer,
@intCast(vertex_buf_size * @sizeOf(formats.Vector3)),
null,
gl.DYNAMIC_STORAGE_BIT,
);
gl.namedBufferStorage(
tangents.buffer,
@intCast(vertex_buf_size * @sizeOf(formats.Vector3)),
null,
gl.DYNAMIC_STORAGE_BIT,
);
gl.namedBufferStorage(
uvs.buffer,
@intCast(vertex_buf_size * @sizeOf(formats.Vector2)),
null,
gl.DYNAMIC_STORAGE_BIT,
);
gl.namedBufferStorage(
indices.buffer,
@intCast(index_buf_size * @sizeOf(formats.Index)),
null,
gl.DYNAMIC_STORAGE_BIT,
);
return .{
.vertex_buddy = vertex_buddy,
.index_buddy = index_buddy,
.vertices = vertices,
.normals = normals,
.tangents = tangents,
.uvs = uvs,
.indices = indices,
};
}
pub fn deinit(self: *Self) void {
self.index_buddy.deinit();
self.vertex_buddy.deinit();
const bufs = [_]gl.GLuint{ self.vertices, self.normals, self.tangents, self.uvs, self.indices };
gl.deleteBuffers(bufs.len, &bufs);
}
pub fn alloc(self: *Self, vertex_len: usize, index_len: usize) !Alloc {
const vertex_alloc = try self.vertex_buddy.alloc(vertex_len);
errdefer self.vertex_buddy.free(vertex_alloc);
const index_alloc = try self.index_buddy.alloc(index_len);
errdefer self.index_buddy.free(index_alloc);
return Alloc{ .vertex = vertex_alloc, .index = index_alloc };
}
pub fn free(self: *Self, allocation: Alloc) void {
self.vertex_buddy.free(allocation.vertex);
self.index_buddy.free(allocation.index);
}
};