From 67a0c4e4174c720c8aac7ec6031a67d096f6338e Mon Sep 17 00:00:00 2001 From: Szymon Nowakowski Date: Mon, 10 Nov 2025 00:41:33 +0100 Subject: [PATCH] Port old pipeline logic --- src/Log.zig | 11 +++ src/Samplers.zig | 49 +++++++++ src/game.zig | 252 +++++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 192 +++++++++++++++++++++++++----------- 4 files changed, 447 insertions(+), 57 deletions(-) create mode 100644 src/Log.zig create mode 100644 src/Samplers.zig create mode 100644 src/game.zig diff --git a/src/Log.zig b/src/Log.zig new file mode 100644 index 0000000..3de5e8b --- /dev/null +++ b/src/Log.zig @@ -0,0 +1,11 @@ +const std = @import("std"); + +level: std.log.Level, +message: []const u8, + +pub fn init(level: std.log.Level, message: []const u8) @This() { + return .{ + .level = level, + .message = message, + }; +} diff --git a/src/Samplers.zig b/src/Samplers.zig new file mode 100644 index 0000000..43a4d5f --- /dev/null +++ b/src/Samplers.zig @@ -0,0 +1,49 @@ +const std = @import("std"); + +const sokol = @import("sokol"); + +const sg = sokol.gfx; + +const main = @import("main.zig"); + +pub var sampler_cache: std.AutoHashMapUnmanaged(Descriptor, sg.Sampler) = .{}; + +pub const Descriptor = struct { + wrap_u: sg.Wrap = .REPEAT, + wrap_v: sg.Wrap = .REPEAT, + wrap_w: sg.Wrap = .REPEAT, + min_filter: sg.Filter = .LINEAR, + mag_filter: sg.Filter = .LINEAR, + mipmap_filter: sg.Filter = .LINEAR, +}; + +pub fn getOrCreateSampler(descriptor: Descriptor) !sg.Sampler { + const entry = try sampler_cache.getOrPut(main.allocator, descriptor); + + if (entry.found_existing) { + return entry.value_ptr.*; + } else { + const sampler = sg.makeSampler(.{ + .wrap_u = descriptor.wrap_u, + .wrap_v = descriptor.wrap_v, + .wrap_w = descriptor.wrap_w, + .min_filter = descriptor.min_filter, + .mag_filter = descriptor.mag_filter, + .mipmap_filter = descriptor.mipmap_filter, + }); + + entry.key_ptr.* = descriptor; + entry.value_ptr.* = sampler; + + return sampler; + } +} + +pub fn deinit() void { + var sampler_cache_it = sampler_cache.valueIterator(); + while (sampler_cache_it.next()) |sampler| { + sg.destroySampler(sampler.*); + } + + sampler_cache.deinit(main.allocator); +} diff --git a/src/game.zig b/src/game.zig new file mode 100644 index 0000000..c89d15d --- /dev/null +++ b/src/game.zig @@ -0,0 +1,252 @@ +const std = @import("std"); + +const ig = @import("cimgui"); +const shader = @import("shader"); +const sokol = @import("sokol"); + +const sapp = sokol.app; +const sg = sokol.gfx; +const sglue = sokol.glue; + +const main = @import("main.zig"); +const Samplers = @import("Samplers.zig"); + +var show_console: bool = false; +var show_demo_window: bool = false; + +var vertex_buffer: sg.Buffer = undefined; +var index_buffer: sg.Buffer = undefined; + +var pipeline: sg.Pipeline = undefined; + +var point_light_buffer: sg.Buffer = undefined; +var directional_light_buffer: sg.Buffer = undefined; +var sampler: sg.Sampler = undefined; +var base_color_texture: sg.Image = undefined; +var occlusion_roughness_metallic_texture: sg.Image = undefined; +var normal_texture: sg.Image = undefined; + +var bindings: sg.Bindings = undefined; + +pub fn init() void { + vertex_buffer = sg.makeBuffer(.{ + .data = sg.asRange(&[_]f32{ + // positionOS texCoord normalOS tangentOS + -0.5, -0.5, 0, 0, 1, 0, 0, 1, 1, 0, 0, -1, + 0.5, -0.5, 0, 1, 1, 0, 0, 1, 1, 0, 0, -1, + -0.5, 0.5, 0, 0, 0, 0, 0, 1, 1, 0, 0, -1, + 0.5, 0.5, 0, 1, 0, 0, 0, 1, 1, 0, 0, -1, + }), + .usage = .{ + .immutable = true, + .vertex_buffer = true, + }, + }); + + index_buffer = sg.makeBuffer(.{ + .data = sg.asRange(&[_]u16{ 0, 1, 2, 2, 1, 3 }), + .usage = .{ + .immutable = true, + .index_buffer = true, + }, + }); + + pipeline = sg.makePipeline(.{ + .shader = sg.makeShader(shader.programShaderDesc(sg.queryBackend())), + .layout = blk: { + var ret: sg.VertexLayoutState = .{}; + ret.attrs[shader.ATTR_program_positionOS].format = .FLOAT3; + ret.attrs[shader.ATTR_program_texCoord].format = .FLOAT2; + ret.attrs[shader.ATTR_program_normalOS].format = .FLOAT3; + ret.attrs[shader.ATTR_program_tangentOS].format = .FLOAT4; + break :blk ret; + }, + .primitive_type = .TRIANGLES, + .index_type = .UINT16, + .cull_mode = .BACK, + .face_winding = .CCW, + }); + + point_light_buffer = sg.makeBuffer(.{ + .size = @sizeOf([4]shader.PointLight), + .usage = .{ + .stream_update = true, + .storage_buffer = true, + }, + }); + + directional_light_buffer = sg.makeBuffer(.{ + .size = @sizeOf([4]shader.DirectionalLight), + .usage = .{ + .stream_update = true, + .storage_buffer = true, + }, + }); + + sampler = Samplers.getOrCreateSampler(.{ + .wrap_u = .REPEAT, + .wrap_v = .REPEAT, + .wrap_w = .REPEAT, + .min_filter = .LINEAR, + .mag_filter = .LINEAR, + .mipmap_filter = .LINEAR, + }) catch unreachable; + + base_color_texture = sg.makeImage(.{ + .type = .ARRAY, + .usage = .{ .immutable = true }, + .width = 16, + .height = 16, + .num_slices = 16, + .pixel_format = .RGBA8, + .data = blk: { + var ret: sg.ImageData = .{}; + ret.mip_levels[0] = sg.asRange(&[_]u8{ 255, 255, 255, 255 } ** (16 * 16 * 16)); + break :blk ret; + }, + }); + + occlusion_roughness_metallic_texture = sg.makeImage(.{ + .type = .ARRAY, + .usage = .{ .immutable = true }, + .width = 1, + .height = 1, + .num_slices = 16, + .pixel_format = .RGBA8, + .data = blk: { + var ret: sg.ImageData = .{}; + ret.mip_levels[0] = sg.asRange(&[_]u8{ 255, 255, 0, 255 } ** (1 * 1 * 16)); + break :blk ret; + }, + }); + + normal_texture = sg.makeImage(.{ + .type = .ARRAY, + .usage = .{ .immutable = true }, + .width = 128, + .height = 128, + .num_slices = 16, + .pixel_format = .RGBA8, + .data = blk: { + var ret: sg.ImageData = .{}; + ret.mip_levels[0] = sg.asRange(&[_]u8{ 127, 127, 255, 255 } ** (128 * 128 * 16)); + break :blk ret; + }, + }); + + bindings = .{}; + + bindings.vertex_buffers[0] = vertex_buffer; + bindings.index_buffer = index_buffer; + + bindings.views[shader.VIEW_Point_Lights] = sg.makeView(.{ + .storage_buffer = .{ .buffer = point_light_buffer }, + }); + bindings.views[shader.VIEW_Directional_Lights] = sg.makeView(.{ + .storage_buffer = .{ .buffer = directional_light_buffer }, + }); + bindings.views[shader.VIEW__BaseColorTexture] = sg.makeView(.{ + .texture = .{ .image = base_color_texture }, + }); + bindings.views[shader.VIEW__OcclusionRoughnessMetallicTexture] = sg.makeView(.{ + .texture = .{ .image = occlusion_roughness_metallic_texture }, + }); + bindings.views[shader.VIEW__NormalTexture] = sg.makeView(.{ + .texture = .{ .image = normal_texture }, + }); + + bindings.samplers[0] = sampler; +} + +pub fn deinit() void {} + +pub fn update(dt: f64) void { + _ = dt; + + if (show_console) { + showConsole(); + } + + if (show_demo_window) { + ig.igShowDemoWindow(&show_demo_window); + } + + sg.beginPass(.{ + .action = blk: { + var ret: sg.PassAction = .{}; + + ret.colors[0] = .{ + .load_action = .CLEAR, + .store_action = .STORE, + .clear_value = .{ .r = 0, .g = 0, .b = 0, .a = 1 }, + }; + + ret.depth = .{ + .load_action = .CLEAR, + .store_action = .STORE, + .clear_value = 0, + }; + + break :blk ret; + }, + .swapchain = sglue.swapchain(), + }); + + sg.applyPipeline(pipeline); + sg.applyBindings(bindings); + + sg.applyUniforms(shader.UB_Global_Uniforms_Vertex, sg.asRange(&shader.GlobalUniformsVertex{ + ._MatrixVStoCS = .{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }, + ._MatrixWStoVS = .{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }, + })); + + sg.applyUniforms(shader.UB_Object_Uniforms, sg.asRange(&shader.ObjectUniforms{ + ._MatrixOStoWS = .{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }, + ._MatrixOStoWSNormal = .{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }, + })); + + sg.applyUniforms(shader.UB_Global_Uniforms_Fragment, sg.asRange(&shader.GlobalUniformsFragment{ + ._MatrixWStoVS = .{ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 }, + ._AmbientLight = .{ 0.1, 0.1, 0.1 }, + ._PointLightCount = 0, + ._DirectionalLightCount = 0, + })); + + sg.applyUniforms(shader.UB_Material_Uniforms, sg.asRange(&shader.MaterialUniforms{ + ._TextureIndex = 0, + })); + + sg.draw(0, 6, 1); + sg.endPass(); +} + +pub fn onKeyDown(key_code: sapp.Keycode, mods: u32) void { + const no_mods = mods == 0; + + if (key_code == .GRAVE_ACCENT and no_mods) { + show_console = !show_console; + } + + if (key_code == .F1 and no_mods) { + show_demo_window = true; + } +} + +fn showConsole() void { + const display_size = ig.igGetIO().*.DisplaySize; + ig.igSetNextWindowPos(.{ .x = 0, .y = 0 }, 0); + ig.igSetNextWindowSize(.{ .x = display_size.x, .y = main.min_window_height }, ig.ImGuiCond_Once); + if (ig.igBegin("Console", null, ig.ImGuiWindowFlags_NoTitleBar | ig.ImGuiWindowFlags_NoResize | ig.ImGuiWindowFlags_NoMove | ig.ImGuiWindowFlags_NoCollapse)) { + for (main.logs.items) |log| { + ig.igPushStyleColorImVec4(ig.ImGuiCol_Text, switch (log.level) { + .err => .{ .x = 1, .y = 0, .z = 0, .w = 1 }, + .warn => .{ .x = 1, .y = 1, .z = 0, .w = 1 }, + .info => .{ .x = 1, .y = 1, .z = 1, .w = 1 }, + .debug => .{ .x = 0.5, .y = 0.5, .z = 0.5, .w = 1 }, + }); + ig.igTextUnformattedEx(log.message.ptr, log.message.ptr + log.message.len); + ig.igPopStyleColor(); + } + ig.igEnd(); + } +} diff --git a/src/main.zig b/src/main.zig index bc49072..b59538a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,3 +1,5 @@ +const std = @import("std"); + const ig = @import("cimgui"); const shader = @import("shader"); const sokol = @import("sokol"); @@ -6,19 +8,24 @@ const sapp = sokol.app; const sg = sokol.gfx; const sglue = sokol.glue; const simgui = sokol.imgui; -const slog = sokol.log; -var show_demo_window = true; +const game = @import("game.zig"); +const Log = @import("Log.zig"); -const main_action = blk: { - var ret: sg.PassAction = .{}; +pub const min_framerate = 30.0; +pub const min_frametime = 1.0 / min_framerate; +pub const min_window_height = 360; +pub const min_window_width = 640; +pub const temp_allocator_capacity = 16 * 1024 * 1024; +pub const window_title = "voxel-game"; - ret.colors[0] = .{ - .load_action = .CLEAR, - .clear_value = .{ .r = 0.0, .g = 0.0, .b = 0.0, .a = 1.0 }, - }; +pub var allocator: std.mem.Allocator = undefined; +pub var temp_allocator: std.mem.Allocator = undefined; - break :blk ret; +pub var logs: std.ArrayListUnmanaged(Log) = .empty; + +pub const std_options: std.Options = .{ + .logFn = logFn, }; const imgui_action = blk: { @@ -31,72 +38,38 @@ const imgui_action = blk: { break :blk ret; }; -var bindings: sg.Bindings = .{}; -var pipeline: sg.Pipeline = .{}; - fn init() callconv(.c) void { sg.setup(.{ .environment = sglue.environment(), - .logger = .{ .func = slog.func }, + .logger = .{ .func = sokolLogFn }, }); simgui.setup(.{ - .logger = .{ .func = slog.func }, + .logger = .{ .func = sokolLogFn }, }); - bindings.vertex_buffers[0] = sg.makeBuffer(.{ - .data = sg.asRange(&[_]f32{ - // positionOS texCoord normalOS tangentOS - 0.0, 0.5, 0.5, 0.5, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, - 0.5, -0.5, 0.5, 1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, - -0.5, -0.5, 0.5, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, - }), - }); - - pipeline = sg.makePipeline(.{ - .shader = sg.makeShader(shader.programShaderDesc(sg.queryBackend())), - .layout = blk: { - var ret: sg.VertexLayoutState = .{}; - ret.attrs[shader.ATTR_program_positionOS].format = .FLOAT3; - ret.attrs[shader.ATTR_program_texCoord].format = .FLOAT2; - ret.attrs[shader.ATTR_program_normalOS].format = .FLOAT3; - ret.attrs[shader.ATTR_program_tangentOS].format = .FLOAT4; - break :blk ret; - }, - }); + game.init(); } fn deinit() callconv(.c) void { + game.deinit(); simgui.shutdown(); sg.shutdown(); } fn frame() callconv(.c) void { + const dt = sapp.frameDuration(); + simgui.newFrame(.{ .width = sapp.width(), .height = sapp.height(), - .delta_time = sapp.frameDuration(), + .delta_time = dt, .dpi_scale = sapp.dpiScale(), }); - // --- UPDATE --- + game.update(dt); - if (show_demo_window) { - ig.igShowDemoWindow(&show_demo_window); - } - - // --- MAIN PASS --- - - sg.beginPass(.{ - .action = main_action, - .swapchain = sglue.swapchain(), - }); - sg.applyPipeline(pipeline); - sg.applyBindings(bindings); - sg.draw(0, 3, 1); - sg.endPass(); - - // --- IMGUI PASS --- + // --- BEGIN IMGUI PASS --- sg.beginPass(.{ .action = imgui_action, @@ -105,16 +78,40 @@ fn frame() callconv(.c) void { simgui.render(); sg.endPass(); - // --- + // --- END IMGUI PASS --- sg.commit(); } -fn event(ev: [*c]const sapp.Event) callconv(.c) void { - _ = simgui.handleEvent(ev.*); +fn event(_ev: [*c]const sapp.Event) callconv(.c) void { + const ev: *const sapp.Event = @ptrCast(_ev); + const ig_capture_keyboard = simgui.handleEvent(ev.*); + + if (!ig_capture_keyboard) { + if (ev.type == .KEY_DOWN and !ev.key_repeat) { + game.onKeyDown(ev.key_code, ev.modifiers); + } + } } -pub fn main() void { +pub fn main() !void { + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; + defer _ = gpa.deinit(); + + var fba: std.heap.FixedBufferAllocator = .init(&struct { + pub var buffer: [temp_allocator_capacity]u8 = undefined; + }.buffer); + + allocator = gpa.allocator(); + temp_allocator = fba.threadSafeAllocator(); + + defer { + for (logs.items) |log| { + allocator.free(log.message); + } + logs.deinit(allocator); + } + sapp.run(.{ .init_cb = &init, .cleanup_cb = &deinit, @@ -124,6 +121,87 @@ pub fn main() void { .width = 1280, .height = 720, .icon = .{ .sokol_default = true }, - .logger = .{ .func = slog.func }, + .logger = .{ .func = sokolLogFn }, }); } + +fn sokolLogFn( + maybe_tag: ?[*:0]const u8, + log_level: u32, + log_item: u32, + maybe_message: ?[*:0]const u8, + line_nr: u32, + maybe_filename: ?[*:0]const u8, + user_data: ?*anyopaque, +) callconv(.c) void { + defer { + if (log_level == 0) { + const message = std.mem.span(maybe_message) orelse ""; + @panic(message); + } + } + + const _SLOG_LINE_LENGTH = 512; + + _ = user_data; + + var line_buf: [_SLOG_LINE_LENGTH]u8 = undefined; + var writer = std.Io.Writer.fixed(&line_buf); + + if (maybe_tag) |tag| { + writer.print("[{s}]", .{tag}) catch return; + } + + writer.print("[{s}]", .{switch (log_level) { + 0 => "panic", + 1 => "error", + 2 => "warning", + else => "info", + }}) catch return; + + writer.print("[id:{d}]", .{log_item}) catch return; + + if (maybe_filename) |filename| { + writer.print(" {s}:{d}:0: ", .{ filename, line_nr }) catch return; + } else { + writer.print("[line:{d}]", .{line_nr}) catch return; + } + + if (maybe_message) |message| { + writer.print("\n\t{s}", .{message}) catch return; + } + + writer.print("\n\n", .{}) catch return; + + if (log_level == 0) { + writer.print("ABORTING because of [panic]\n", .{}) catch return; + } + + const level: std.log.Level = switch (log_level) { + 0, 1 => .err, + 2 => .warn, + else => .info, + }; + + const full_message = allocator.dupe(u8, writer.buffered()) catch return; + + std.debug.print("{s}", .{full_message}); + + logs.append(allocator, .init(level, full_message)) catch { + allocator.free(full_message); + return; + }; +} + +fn logFn( + comptime level: std.log.Level, + comptime _: @Type(.enum_literal), + comptime fmt: []const u8, + args: anytype, +) void { + const message = std.fmt.allocPrint(allocator, fmt, args) catch return; + logs.append(allocator, .init(level, message)) catch { + allocator.free(message); + return; + }; +}