1683 lines
52 KiB
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);
|
|
}
|
|
};
|