// sdl3_gpu is like 8 characterso // so instead of refering to it by it's full name every time // // we will just call it sgpu. pub const Renderer = struct { allocator: std.mem.Allocator, totalTime: f64 = 0, device: *gpu.GPUDevice = undefined, shaderType: []const u8 = undefined, shaderSuffix: []const u8 = undefined, shaderformat: gpu.GPUShaderFormat = undefined, entrypoint: []const u8 = "main", testPipeline: *gpu.GPUGraphicsPipeline = undefined, meshPipe: *gpu.GPUGraphicsPipeline = undefined, scissor: gpu.Rect = undefined, // .{ .x = 0, .y = 0, .w = 1600, .h = 900 }, colorBuffer: *gpu.GPUBuffer = undefined, colorBufferTransfer: *gpu.GPUTransferBuffer = undefined, window: *sdl3.Window = undefined, activeCamera: ?*rend.CameraComponent = null, ssboScene: *gpu.GPUBuffer = undefined, ssboSceneUpload: *gpu.GPUTransferBuffer = undefined, meshPool: *MeshPool = undefined, uploads: std.ArrayListUnmanaged(struct { ptr: *anyopaque, func: *const fn (*anyopaque, *gpu.GPUCopyPass) void }) = .{}, uploadCleanup: std.ArrayListUnmanaged(struct { ptr: *anyopaque, func: *const fn (*anyopaque) void }) = .{}, postMesh: std.ArrayListUnmanaged(struct { ptr: *anyopaque, func: *const fn (*anyopaque, *gpu.GPUCommandBuffer, *gpu.GPURenderPass) void }) = .{}, postRenders: std.ArrayListUnmanaged(struct { ptr: *anyopaque, func: *const fn (*anyopaque, *gpu.GPUCommandBuffer) void }) = .{}, destroys: std.ArrayListUnmanaged(struct { ptr: *anyopaque, func: *const fn (*anyopaque) void }) = .{}, preDraws: std.ArrayListUnmanaged(struct { ptr: *anyopaque, func: *const fn (*anyopaque, *gpu.GPUCommandBuffer) void }) = .{}, depthTexture: *gpu.GPUTexture = undefined, textureList: *TextureList = undefined, blockySampler: *gpu.GPUSampler = undefined, defaultTexture: *rend.Texture = undefined, lightPosition: core.Vectorf = .{}, debugDrawSys: *DebugDrawSystem = undefined, directionalLightYaw: f32 = 0.0, directionalLightColor: core.colors.Color = .{ .r = 0.8, .g = 0.8, .b = 0.9 }, directionalLightDir: core.Vectorf = core.Vectorf.new(0.5, 0.25, -1).normalize(), // transients DO NOT TOUCH swapchainTexture: ?*gpu.GPUTexture = undefined, pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This()); pub const MaxObjectCount = 50000; pub fn init(allocator: std.mem.Allocator) !*@This() { const self = try allocator.create(@This()); self.* = .{ .allocator = allocator, }; try core.defineComponent(rend.MeshComponent, allocator); try core.defineComponent(rend.CameraComponent, allocator); return self; } pub fn startRenderer(self: *@This()) !void { self.device = gpu.createGPUDevice(.{ .shaderformatSpirv = true, .shaderformatDxil = true, .shaderformatMsl = true, }, true, null); self.window = platform.getInstance().window; self.scissor = .{ .x = 0, .y = 0, .w = platform.getInstance().windowExtent.x, .h = platform.getInstance().windowExtent.y }; if (!self.device.claimWindowForGPUDevice(self.window)) return error.UnableToClaimGpu; try self.discoverFormats(); core.engine_log("sgpu device created, using shader format: {s}", .{self.shaderSuffix}); core.engine_log("using renderer... scientist", .{}); try self.createMeshPipeline(); try self.createBuffers(); try self.createDepthTexture(); self.meshPool = try self.createRendererObject(MeshPool); self.textureList = try self.createRendererEngineObject(TextureList); try self.createSamplers(); try assets.load( assets.MakeImportRefOptions( "Texture", "t_default", .{ .path = "textures/texture_sample.png" }, ), ); try assets.load( assets.MakeImportRefOptions( "Mesh", "m_default_cube", .{ .path = "meshes/primitive_box.obj" }, ), ); var defaultTextureName = core.MakeName("t_default"); self.defaultTexture = getTexture(&defaultTextureName).?; self.debugDrawSys = try self.createRendererEngineObject(DebugDrawSystem); } pub fn createSamplers(self: *@This()) !void { core.engine_log("creating blocky sampler", .{}); self.blockySampler = self.device.createGPUSampler(&std.mem.zeroInit(gpu.GPUSamplerCreateInfo, .{ .min_filter = .filterNearest, .mag_filter = .filterNearest, .mipmap_mode = .samplermipmapmodeNearest, .address_mode_u = .sampleraddressmodeRepeat, .address_mode_v = .sampleraddressmodeRepeat, .address_mode_w = .sampleraddressmodeRepeat, })); } // registers a pre-existing render object, does not add to destroys pub fn registerRendererObject(self: *@This(), T: type, object: *anyopaque) !void { if (@hasDecl(T, "onUpload")) { try self.uploads.append(self.allocator, .{ .ptr = object, .func = T.onUpload }); } if (@hasDecl(T, "onUploadCleanup")) { try self.uploadCleanup.append(self.allocator, .{ .ptr = object, .func = T.onUploadCleanup }); } if (@hasDecl(T, "postRender")) { try self.postRenders.append(self.allocator, .{ .ptr = object, .func = T.postRender }); } if (@hasDecl(T, "postMesh")) { try self.postMesh.append(self.allocator, .{ .ptr = object, .func = T.postMesh }); } if (@hasDecl(T, "onPreDraw")) { try self.preDraws.append(self.allocator, .{ .ptr = object, .func = T.onPreDraw }); } } pub fn createRendererEngineObject(self: *@This(), T: type) !*T { const object = try core.createObject(T, .{}); try object.setup(self.device); try self.registerRendererObject(T, object); return object; } pub fn createRendererObject(self: *@This(), T: type) !*T { const object = try T.create(self.device, self.allocator, .{}); try self.registerRendererObject(T, object); try self.destroys.append(self.allocator, .{ .ptr = object, .func = T.destroy }); return object; } fn createDepthTexture(self: *@This()) !void { var gci = std.mem.zeroes(gpu.GPUTextureCreateInfo); gci.type = .texturetype2d; gci.format = .textureformatD32Float; gci.width = @intCast(self.scissor.w); gci.height = @intCast(self.scissor.h); gci.layer_count_or_depth = 1; gci.num_levels = 1; gci.usage = .{ .textureusageDepthStencilTarget = true }; self.depthTexture = self.device.createGPUTexture(&gci); } pub fn discoverFormats(self: *@This()) !void { const formats = self.device.getGPUShaderFormats(); if (formats.shaderformatSpirv) { self.shaderType = "spv"; // shaderformatSpirv self.shaderSuffix = ".spv"; self.shaderformat = .{ .shaderformatSpirv = true }; } else if (formats.shaderformatMsl) { self.shaderType = "msl"; // shaderformatSpirv self.shaderSuffix = ".msl"; self.entrypoint = "main0"; self.shaderformat = .{ .shaderformatMsl = true }; } else if (formats.shaderformatDxil) { self.shaderType = "dxil"; // shaderformatSpirv self.shaderSuffix = ".dxil"; self.shaderformat = .{ .shaderformatDxil = true }; } } fn addAttribute(list: *std.ArrayList(gpu.GPUVertexAttribute), offset: *u32, size: u32, format: gpu.GPUVertexElementFormat) !void { try list.append(.{ .location = @intCast(list.items.len), .offset = offset.*, .format = format, .buffer_slot = 0 }); offset.* = offset.* + size; } pub fn createMeshPipeline(self: *@This()) !void { const vertex = try self.loadShader("meshes.vert", meshes_vert.LoadArgs); const fragment = try self.loadShader("lit_mesh.frag", lit_mesh_frag.LoadArgs); var pci = std.mem.zeroes(gpu.GPUGraphicsPipelineCreateInfo); pci.vertex_shader = vertex; pci.fragment_shader = fragment; var attributes = std.ArrayList(gpu.GPUVertexAttribute).init(self.allocator); defer attributes.deinit(); var offset: u32 = 0; { try addAttribute(&attributes, &offset, @sizeOf(f32) * 3, .vertexelementformatFloat3); try addAttribute(&attributes, &offset, @sizeOf(f32) * 3, .vertexelementformatFloat3); try addAttribute(&attributes, &offset, @sizeOf(f32) * 4, .vertexelementformatFloat4); try addAttribute(&attributes, &offset, @sizeOf(f32) * 2, .vertexelementformatFloat2); try addAttribute(&attributes, &offset, @sizeOf(u32), .vertexelementformatUint); } pci.vertex_input_state = .{ .num_vertex_buffers = 1, .vertex_buffer_descriptions = &[_]gpu.GPUVertexBufferDescription{ .{ .slot = 0, .pitch = @sizeOf(rend.MeshVertex), .input_rate = .vertexinputrateVertex, .instance_step_rate = 0 }, }, .num_vertex_attributes = @intCast(attributes.items.len), .vertex_attributes = @ptrCast(attributes.items.ptr), }; pci.depth_stencil_state.compare_op = .compareopLess; pci.depth_stencil_state.enable_depth_test = true; pci.depth_stencil_state.enable_depth_write = true; pci.depth_stencil_state.write_mask = 0xff; pci.target_info.has_depth_stencil_target = true; pci.target_info.depth_stencil_format = .textureformatD32Float; pci.target_info.num_color_targets = 1; pci.target_info.color_target_descriptions = &[_]gpu.GPUColorTargetDescription{ .{ .format = self.device.getGPUSwapchainTextureFormat(self.window), .blend_state = std.mem.zeroes(gpu.GPUColorTargetBlendState), }, }; pci.rasterizer_state.fill_mode = .fillmodeFill; self.meshPipe = self.device.createGPUGraphicsPipeline(&pci); } pub fn createBuffers(self: *@This()) !void { // ssbo buffer // todo.. sparse uploads self.ssboScene = self.device.createGPUBuffer(&.{ .usage = .{ .bufferusageGraphicsStorageRead = true, .bufferusageVertex = true }, .size = MaxObjectCount * @sizeOf(meshes_vert.Scene), .props = 0, }); self.ssboSceneUpload = self.device.createGPUTransferBuffer(&.{ .usage = .transferbufferusageUpload, .size = MaxObjectCount * @sizeOf(meshes_vert.Scene), .props = 0, }); } pub fn uploadSSBOs(self: *@This(), copyPass: *gpu.GPUCopyPass) !void { const container = rend.MeshComponent.BaseContainer; const uploadCount: usize = @min(container.dense.items.len, MaxObjectCount); { const uploadMapped: [*]meshes_vert.Scene = @ptrCast(@alignCast(self.device.mapGPUTransferBuffer(self.ssboSceneUpload, true))); defer self.device.unmapGPUTransferBuffer(self.ssboSceneUpload); // iterate over MeshComponents for (0..uploadCount) |i| { // const object = &container.dense.items[i].value; const objectId = container.dense.items[i].sparseIndex; var transform = core.zm.identity(); if (core.Scene.SceneObjectContainer.get(objectId, ._repr)) |repr| { transform = repr.transform; } uploadMapped[i].Model = @bitCast(transform); } } if (uploadCount == 0) return; copyPass.uploadToGPUBuffer(&.{ .transfer_buffer = self.ssboSceneUpload, .offset = 0 }, &.{ .buffer = self.ssboScene, .offset = 0, .size = @intCast(uploadCount * @sizeOf(meshes_vert.Scene)), }, true); } pub fn loadShader( self: *@This(), shaderName: []const u8, loadArgs: ShaderLoadArgs, ) !*gpu.GPUShader { const contentPath = try std.fmt.allocPrint(self.allocator, "_shaders/{s}/{s}{s}", .{ self.shaderType, shaderName, self.shaderSuffix }); defer self.allocator.free(contentPath); var stage: gpu.GPUShaderStage = undefined; if (std.mem.endsWith(u8, shaderName, ".vert")) { stage = .shaderstageVertex; } else if (std.mem.endsWith(u8, shaderName, ".frag")) { stage = .shaderstageFragment; } else { return error.NotImplemented; } core.engine_log("creating shader {s} => {s} {any} args: {any}", .{ shaderName, contentPath, stage, loadArgs }); const mapping = try core.fs().loadFile(contentPath); defer core.fs().unmap(mapping); const sci = gpu.GPUShaderCreateInfo{ .code = @ptrCast(mapping.bytes.ptr), .entrypoint = @ptrCast(self.entrypoint.ptr), .format = self.shaderformat, .code_size = mapping.bytes.len - 1, .stage = stage, .num_samplers = loadArgs.num_samplers, .num_storage_textures = loadArgs.num_storage_textures, // The number of storage textures defined in the shader. .num_storage_buffers = loadArgs.num_storage_buffers, // The number of storage buffers defined in the shader. .num_uniform_buffers = loadArgs.num_uniform_buffers, // The number of uniform buffers defined in the shader. .props = 0, }; const rv = self.device.createGPUShader(&sci); return rv; } pub fn uploadUniforms(self: *@This(), cmd: *gpu.GPUCommandBuffer) !void { { var data = std.mem.zeroes([@sizeOf(meshes_vert.Uniforms) / 8 + 1]usize); const ptr: *meshes_vert.Uniforms = @ptrCast(@alignCast(&data)); if (self.activeCamera) |camera| { ptr.ViewProjection = @bitCast(camera.final); } ptr.time = @floatCast(self.totalTime); cmd.pushGPUVertexUniformData(0, &data, @sizeOf(meshes_vert.Uniforms)); } { var data = std.mem.zeroes([@sizeOf(lit_mesh_frag.Uniforms) / 8 + 1]usize); const ptr: *lit_mesh_frag.Uniforms = @ptrCast(@alignCast(&data)); ptr.time = @floatCast(self.totalTime); var position: core.Vectorf = .{}; if (self.activeCamera) |cam| { position = cam.finalPos; } const resolved = core.Rotation.eulerY(core.radians(self.directionalLightYaw)).rotateVector(self.directionalLightDir); ptr.viewPos = @bitCast(position.toZm()); ptr.lightPosition = @bitCast(self.lightPosition.toZm()); ptr.directionalLight = @bitCast(resolved.toZm()); ptr.directionalLightColor = @bitCast(self.directionalLightColor); cmd.pushGPUFragmentUniformData(0, &data, @sizeOf(lit_mesh_frag.Uniforms)); } } pub fn frameUploads(self: *@This()) void { const cmd = self.device.acquireGPUCommandBuffer(); // const buffer = self.device.mapGPUTransferBuffer(self.colorBufferTransfer, true); // var b: [*][4]f32 = @ptrCast(@alignCast(buffer)); // b[0] = .{ @floatCast(std.math.sin(self.totalTime * 3 * 2 + 0.8) * 0.2 + 0.8), 0.4, 0.4, 1.0 }; // b[1] = .{ 0.4, @floatCast(std.math.sin(self.totalTime * 2 * 2 + 0.3) * 0.2 + 0.8), 0.4, 1.0 }; // b[2] = .{ 0.4, 0.4, @floatCast(std.math.sin(self.totalTime * 4 * 2) * 0.2 + 0.8), 1.0 }; // self.device.unmapGPUTransferBuffer(self.colorBufferTransfer); if (self.activeCamera) |camera| { camera.resolve(); } const copyPass = cmd.beginGPUCopyPass(); // copyPass.uploadToGPUBuffer(&.{ .transfer_buffer = self.colorBufferTransfer, .offset = 0 }, &.{ // .buffer = self.colorBuffer, // .offset = 0, // .size = 4 * 12, // }, true); try self.uploadSSBOs(copyPass); for (self.uploads.items) |upload| { upload.func(upload.ptr, copyPass); } copyPass.endGPUCopyPass(); if (!cmd.submitGPUCommandBuffer()) { core.graphics_log("submit gpu command buffer SDL ERROR: {s}", .{sdl3.getError()}); } for (self.uploadCleanup.items) |uploadCleanup| { uploadCleanup.func(uploadCleanup.ptr); } } pub fn tick(self: *@This(), dt: f64) void { self.totalTime += dt; const cmd = self.device.acquireGPUCommandBuffer(); self.frameUploads(); try self.uploadUniforms(cmd); for (self.preDraws.items) |interface| { interface.func(interface.ptr, cmd); } if (cmd.waitAndAcquireGPUSwapchainTexture(self.window, @ptrCast(&self.swapchainTexture), null, null)) { if (self.swapchainTexture == null) { _ = cmd.submitGPUCommandBuffer(); return; } var targetInfo: gpu.GPUColorTargetInfo = std.mem.zeroes(gpu.GPUColorTargetInfo); targetInfo.texture = self.swapchainTexture.?; targetInfo.clear_color = .{ .r = 0.1, .g = 0.1, .b = 0.1, .a = 1.0 }; targetInfo.load_op = .loadopClear; targetInfo.store_op = .storeopStore; var depthTarget = std.mem.zeroes(gpu.GPUDepthStencilTargetInfo); depthTarget.texture = self.depthTexture; depthTarget.load_op = .loadopClear; depthTarget.store_op = .storeopDontCare; depthTarget.stencil_load_op = .loadopDontCare; depthTarget.stencil_store_op = .storeopDontCare; depthTarget.clear_depth = 1.0; depthTarget.clear_stencil = 0; const renderpass = cmd.beginGPURenderPass(&targetInfo, 1, &depthTarget); //renderpass.bindGPUGraphicsPipeline(self.testPipeline); renderpass.bindGPUGraphicsPipeline(self.meshPipe); renderpass.bindGPUVertexStorageBuffers(0, &self.ssboScene, 1); renderpass.bindGPUVertexBuffers(0, &.{ .buffer = self.meshPool.vertexBuffer, .offset = 0 }, 1); renderpass.bindGPUIndexBuffer(&.{ .buffer = self.meshPool.indexBuffer, .offset = 0 }, .indexelementsize32bit); // todo move into materials system renderpass.setGPUScissor(&self.scissor); //SDL_BindGPUFragmentSamplers(renderPass, 0, &(SDL_GPUTextureSamplerBinding){ .texture = Texture, .sampler = Samplers[CurrentSamplerIndex] }, 1); //rendering each mesh { const container = rend.MeshComponent.BaseContainer; const uploadCount: usize = @min(container.dense.items.len, MaxObjectCount); for (0..uploadCount) |i| { // core.engine_log("pushing draw {d}", .{i}); const component = &container.dense.items[i].value; // todo move this to slow tick? only update once every 1 second or so? if (component.mesh == null) { component.updateMesh(); } if (component.texture == null) { component.updateTexture(); } var t = component.texture; if (t == null) { t = self.defaultTexture; } renderpass.bindGPUFragmentSamplers(0, &.{ .texture = t.?.texture, .sampler = self.blockySampler }, 1); if (component.mesh) |mesh| { // core.engine_log("pushing draw for instance {d}", .{i}); renderpass.drawGPUIndexedPrimitives(mesh.index.size, 1, mesh.index.start, @intCast(mesh.vertex.start), @intCast(i)); } } } for (self.postMesh.items) |interface| { interface.func(interface.ptr, cmd, renderpass); } renderpass.endGPURenderPass(); for (self.postRenders.items) |interface| { interface.func(interface.ptr, cmd); } // renderpass.drawGPUPrimitives(3, 1, 0, 0); } _ = cmd.submitGPUCommandBuffer(); } pub fn deinit(self: *@This()) void { // self.device.releaseGPUGraphicsPipeline(self.testPipeline); for (self.destroys.items) |d| { d.func(d.ptr); } self.postMesh.deinit(self.allocator); self.uploadCleanup.deinit(self.allocator); self.preDraws.deinit(self.allocator); self.postRenders.deinit(self.allocator); self.uploads.deinit(self.allocator); self.destroys.deinit(self.allocator); self.allocator.destroy(self); } }; pub var gRenderer: *Renderer = undefined; pub var gAllocator: std.mem.Allocator = undefined; const rend = @import("../rend.zig"); const std = @import("std"); const assets = @import("assets"); const core = @import("core"); const platform = @import("platform"); const sdl3 = @import("sdl3"); const gpu = sdl3.gpu; const meshes_vert = @import("meshes.vert"); const lit_mesh_frag = @import("lit_mesh.frag"); const sample_vert = @import("sample.vert"); const MeshVertices = meshes_vert.Scene; const MeshUniforms = meshes_vert.Uniforms; const ShaderLoadArgs = sdl3.shaderTypes.ShaderLoadArgs; pub const mesh_pool = @import("mesh-pool.zig"); pub const MeshPool = mesh_pool.MeshPool; pub const TextureList = @import("TextureList.zig"); const DebugDrawSystem = @import("DebugDrawSystem.zig"); // debug api pub fn reloadShaders() !void { _ = core.shell.runCmd(gRenderer.allocator, &.{ "python", "../tools/scripts/cookShaders.py" }, ".") catch { core.graphics_logs("shader cook script failed"); return; }; try gRenderer.createMeshPipeline(); } // ====== renderer API ======= pub fn createInstance() !void { gRenderer = try core.createObject(Renderer, .{ .can_tick = true, .isCore = true }); gAllocator = gRenderer.allocator; } pub fn start() !void { try gRenderer.startRenderer(); } pub fn shutdown() void {} pub fn context() *Renderer { return gRenderer; } pub fn setActiveCamera(camera: ?*rend.CameraComponent) void { gRenderer.activeCamera = camera; } pub fn createRendererObject(comptime T: type) !*T { return try gRenderer.createRendererObject(T); } pub fn registerRendererObject(comptime T: type, object: *anyopaque) !void { return try gRenderer.registerRendererObject(T, object); } pub const getMesh = mesh_pool.getMesh; pub const getMeshByName = mesh_pool.getMeshByName; pub const pushMeshUpdate = mesh_pool.pushMeshUpdate; pub fn getTexture(name: *core.Name) ?*rend.Texture { return gRenderer.textureList.map.get(name.handle()); } pub const GPUTextureType = gpu.GPUTexture;