const Resources = @import("./Resources.zig"); const coral = @import("./coral.zig"); const ext = @cImport({ @cInclude("SDL2/SDL.h"); }); const ona = @import("ona"); const sokol = @import("sokol"); const spirv = @import("./spirv.zig"); const std = @import("std"); const Frame = struct { texture_batch_buffers: ona.stack.Sequential(sokol.gfx.Buffer), quad_index_buffer: sokol.gfx.Buffer, quad_vertex_buffer: sokol.gfx.Buffer, drawn_count: usize = 0, flushed_count: usize = 0, current_source_texture: coral.Texture = .default, current_target_texture: coral.Texture = .backbuffer, current_effect: coral.Effect = .default, const DrawTexture = extern struct { transform: coral.Transform2D, tint: @Vector(4, u8) = @splat(std.math.maxInt(u8)), depth: f32 = 0, texture_offset: @Vector(2, f32) = @splat(0), texture_size: @Vector(2, f32) = @splat(1), }; const batches_per_buffer = 512; pub fn deinit(self: *Frame) void { for (self.texture_batch_buffers.values) |buffer| { sokol.gfx.destroyBuffer(buffer); } self.texture_batch_buffers.deinit(); self.* = undefined; } pub fn init() !Frame { const Vertex = struct { xy: @Vector(2, f32), uv: @Vector(2, f32), }; const quad_index_buffer = sokol.gfx.makeBuffer(.{ .data = sokol.gfx.asRange(&[_]u16{0, 1, 2, 0, 2, 3}), .type = .INDEXBUFFER, }); const quad_vertex_buffer = sokol.gfx.makeBuffer(.{ .data = sokol.gfx.asRange(&[_]Vertex{ .{.xy = .{-0.5, -0.5}, .uv = .{0, 1}}, .{.xy = .{0.5, -0.5}, .uv = .{1, 1}}, .{.xy = .{0.5, 0.5}, .uv = .{1, 0}}, .{.xy = .{-0.5, 0.5}, .uv = .{0, 0}}, }), .type = .VERTEXBUFFER, }); return .{ .texture_batch_buffers = .{.allocator = ona.heap.allocator}, .quad_index_buffer = quad_index_buffer, .quad_vertex_buffer = quad_vertex_buffer, }; } pub fn draw_texture(self: *Frame, resources: *Resources, command: coral.Commands.DrawTextureCommand) !void { if (command.texture != self.current_source_texture) { self.flush(resources); } self.current_source_texture = command.texture; const has_filled_current_buffer = (self.drawn_count % batches_per_buffer) == 0; const buffer_count = self.drawn_count / batches_per_buffer; if (has_filled_current_buffer and buffer_count == self.texture_batch_buffers.len()) { const instance_buffer = sokol.gfx.makeBuffer(.{ .size = @sizeOf(DrawTexture) * batches_per_buffer, .usage = .STREAM, }); errdefer sokol.gfx.destroyBuffer(instance_buffer); try self.texture_batch_buffers.push_grow(instance_buffer); } _ = sokol.gfx.appendBuffer(self.texture_batch_buffers.get().?, sokol.gfx.asRange(&DrawTexture{ .transform = command.transform, })); self.drawn_count += 1; } pub fn finish(self: *Frame, resources: *Resources) void { self.flush(resources); self.drawn_count = 0; self.flushed_count = 0; self.current_source_texture = .default; self.current_target_texture = .backbuffer; self.current_effect = .default; } pub fn flush(self: *Frame, resources: *Resources) void { if (self.flushed_count == self.drawn_count) { return; } var bindings = sokol.gfx.Bindings{ .index_buffer = self.quad_index_buffer, }; bindings.vertex_buffers[vertex_indices.mesh] = self.quad_vertex_buffer; switch (resources.get_texture(self.current_source_texture).?.access) { .render => |render| { bindings.fs.images[0] = render.color_image; bindings.fs.samplers[0] = default_sampler; }, .static => |static| { bindings.fs.images[0] = static.image; bindings.fs.samplers[0] = default_sampler; }, .empty => { @panic("Cannot render empty textures"); }, } const effect = resources.get_effect(self.current_effect).?; sokol.gfx.applyPipeline(effect.pipeline); const texture = resources.get_texture(self.current_target_texture).?; sokol.gfx.applyUniforms(.VS, 0, sokol.gfx.asRange(&orthographic_projection(-1.0, 1.0, .{ .left = 0, .top = 0, .right = @floatFromInt(texture.width), .bottom = @floatFromInt(texture.height), }))); if (effect.properties.len != 0) { sokol.gfx.applyUniforms(.FS, 0, sokol.gfx.asRange(effect.properties)); } while (true) { const buffer_index = self.flushed_count / batches_per_buffer; const buffer_offset = self.flushed_count % batches_per_buffer; const instances_to_flush = @min(batches_per_buffer - buffer_offset, self.drawn_count - self.flushed_count); self.flushed_count += instances_to_flush; bindings.vertex_buffers[vertex_indices.instance] = self.texture_batch_buffers.values[buffer_index]; bindings.vertex_buffer_offsets[vertex_indices.instance] = @intCast(@sizeOf(DrawTexture) * buffer_offset); sokol.gfx.applyBindings(bindings); sokol.gfx.draw(0, 6, @intCast(instances_to_flush)); if (self.flushed_count == self.drawn_count) { break; } } } pub fn set_effect(self: *Frame, resources: *Resources, command: coral.Commands.SetEffectCommand) void { if (command.effect != self.current_effect) { self.flush(resources); } self.current_effect = command.effect; if (resources.get_effect(self.current_effect)) |effect| { @memcpy(effect.properties, command.properties); } } pub fn set_target(self: *Frame, resources: *Resources, command: coral.Commands.SetTargetCommand) void { sokol.gfx.endPass(); var pass = sokol.gfx.Pass{ .action = .{ .stencil = .{ .load_action = .CLEAR, }, }, }; if (command.clear_color) |color| { pass.action.colors[0] = .{ .load_action = .CLEAR, .clear_value = @bitCast(color), }; } else { pass.action.colors[0] = .{.load_action = .LOAD}; } if (command.clear_depth) |depth| { pass.action.depth = .{ .load_action = .CLEAR, .clear_value = depth, }; } else { pass.action.depth = .{.load_action = .LOAD}; } pass.attachments = switch (resources.get_texture(self.current_target_texture).?.access) { .static => @panic("Cannot render to static textures"), .empty => @panic("Cannot render to empty textures"), .render => |render| render.attachments, }; self.current_target_texture = command.texture; sokol.gfx.beginPass(pass); } }; fn Matrix(comptime n: usize, comptime Element: type) type { return [n]@Vector(n, Element); } var default_sampler: sokol.gfx.Sampler = undefined; const vertex_indices = .{ .mesh = 0, .instance = 1, }; fn orthographic_projection(near: f32, far: f32, viewport: coral.Rect) Matrix(4, f32) { const width = viewport.right - viewport.left; const height = viewport.bottom - viewport.top; return .{ .{2 / width, 0, 0, 0}, .{0, 2 / height, 0, 0}, .{0, 0, 1 / (far - near), 0}, .{-((viewport.left + viewport.right) / width), -((viewport.top + viewport.bottom) / height), near / (near - far), 1}, }; } pub fn process_work(pending_work: *coral.Assets.WorkQueue, window: *ext.SDL_Window) !void { const context = configure_and_create: { var result = @as(c_int, 0); result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_FLAGS, ext.SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_PROFILE_MASK, ext.SDL_GL_CONTEXT_PROFILE_CORE); result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_MAJOR_VERSION, 3); result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_CONTEXT_MINOR_VERSION, 3); result |= ext.SDL_GL_SetAttribute(ext.SDL_GL_DOUBLEBUFFER, 1); if (result != 0) { return error.Unsupported; } break: configure_and_create ext.SDL_GL_CreateContext(window); }; defer ext.SDL_GL_DeleteContext(context); sokol.gfx.setup(.{ .environment = .{ .defaults = .{ .color_format = .RGBA8, .depth_format = .DEPTH_STENCIL, .sample_count = 1, }, }, .logger = .{ .func = sokol.log.func, }, }); defer { sokol.gfx.shutdown(); } var resources = try Resources.init(); defer { resources.deinit(); } var frame = try Frame.init(); defer { frame.deinit(); } default_sampler = sokol.gfx.makeSampler(.{}); while (true) { switch (pending_work.dequeue()) { .load_effect => |load| { const effect = try resources.create_effect(load.desc); if (!load.loaded.resolve(effect)) { std.debug.assert(resources.destroy_effect(effect)); } }, .load_texture => |load| { const texture = try resources.create_texture(load.desc); if (!load.loaded.resolve(texture)) { std.debug.assert(resources.destroy_texture(texture)); } }, .render_frame => |render_frame| { const backbuffer = resources.get_texture(.backbuffer).?; if (backbuffer.width != render_frame.width or backbuffer.height != render_frame.height) { backbuffer.deinit(); backbuffer.* = try Resources.Texture.init(.{ .format = .rgba8, .access = .{ .render = .{ .width = render_frame.width, .height = render_frame.height, }, }, }); } sokol.gfx.beginPass(pass: { var pass = sokol.gfx.Pass{ .action = .{ .stencil = .{ .load_action = .CLEAR, }, .depth = .{ .load_action = .CLEAR, .clear_value = 0, } }, }; pass.action.colors[0] = .{ .load_action = .CLEAR, .clear_value = @bitCast(render_frame.clear_color), }; pass.attachments = resources.get_texture(.backbuffer).?.access.render.attachments; break: pass pass; }); var has_command_params = render_frame.has_command_params; while (has_command_params) |command_params| : (has_command_params = command_params.has_next) { for (command_params.param.submitted_commands()) |command| { try switch (command) { .draw_texture => |draw_texture| frame.draw_texture(&resources, draw_texture), .set_effect => |set_effect| frame.set_effect(&resources, set_effect), .set_target => |set_target| frame.set_target(&resources, set_target), }; } frame.flush(&resources); if (frame.current_target_texture != .backbuffer) { frame.set_target(&resources, .{ .texture = .backbuffer, .clear_color = null, .clear_depth = null, .clear_stencil = null, }); } } sokol.gfx.endPass(); sokol.gfx.beginPass(swapchain_pass: { var pass = sokol.gfx.Pass{ .swapchain = .{ .width = render_frame.width, .height = render_frame.height, .sample_count = 1, .color_format = .RGBA8, .depth_format = .DEPTH_STENCIL, .gl = .{.framebuffer = 0}, }, .action = .{ .stencil = .{.load_action = .CLEAR}, .depth = .{.load_action = .CLEAR}, }, }; pass.action.colors[0] = .{.load_action = .CLEAR}; break: swapchain_pass pass; }); try frame.draw_texture(&resources, .{ .texture = .backbuffer, .transform = .{ .origin = .{@as(f32, @floatFromInt(render_frame.width)) / 2, @as(f32, @floatFromInt(render_frame.height)) / 2}, .xbasis = .{@floatFromInt(render_frame.width), 0}, .ybasis = .{0, @floatFromInt(render_frame.height)}, }, }); frame.finish(&resources); sokol.gfx.endPass(); sokol.gfx.commit(); ext.SDL_GL_SwapWindow(window); render_frame.finished.set(); }, .shutdown => { break; }, .unload_effect => |unload| { if (!resources.destroy_effect(unload.handle)) { @panic("Attempt to unload a non-existent effect"); } }, .unload_texture => |unload| { if (!resources.destroy_texture(unload.handle)) { @panic("Attempt to unload a non-existent texture"); } }, } } } var work_thread: std.Thread = undefined;