// SDL3 GPU backend for lance3d var video = use('sdl3/video') var gpu_mod = use('sdl3/gpu') var blob_mod = use('blob') var io = use('fd') // Private state var _gpu = null var _window = null var _swapchain_format = null var _depth_texture = null var _white_texture = null var _vert_shader = null var _frag_shader = null var _skinned_vert_shader = null var _sampler_nearest = null var _sampler_linear = null var _pipelines = {} var _resolution_w = 640 var _resolution_h = 480 // Initialize the GPU backend function init(opts) { opts = opts || {} _resolution_w = opts.width || 640 _resolution_h = opts.height || 480 _window = video.window({ title: opts.title || "lance3d", width: _resolution_w, height: _resolution_h }) _gpu =gpu_mod.gpu({ debug: true, shaders_msl: true }) _gpu.claim_window(_window) var vert_code = io.slurp("shaders/retro3d.vert.msl") var frag_code = io.slurp("shaders/retro3d.frag.msl") if (!vert_code || !frag_code) { log.console("sdl backend: failed to load shaders") return false } _vert_shader =gpu_mod.shader(_gpu, { code: vert_code, stage: "vertex", format: "msl", entrypoint: "vertex_main", num_uniform_buffers: 2 }) _frag_shader =gpu_mod.shader(_gpu, { code: frag_code, stage: "fragment", format: "msl", entrypoint: "fragment_main", num_uniform_buffers: 2, num_samplers: 1 }) _swapchain_format = _gpu.swapchain_format(_window) var skinned_vert_code = io.slurp("shaders/retro3d_skinned.vert.msl") if (skinned_vert_code) { _skinned_vert_shader =gpu_mod.shader(_gpu, { code: skinned_vert_code, stage: "vertex", format: "msl", entrypoint: "vertex_main", num_uniform_buffers: 3 }) } // Pre-create common pipelines get_pipeline(false, "opaque", "back") get_pipeline(true, "opaque", "back") _sampler_nearest =gpu_mod.sampler(_gpu, { min_filter: "nearest", mag_filter: "nearest", u: "repeat", v: "repeat" }) _sampler_linear =gpu_mod.sampler(_gpu, { min_filter: "linear", mag_filter: "linear", u: "repeat", v: "repeat" }) _depth_texture =gpu_mod.texture(_gpu, { width: _resolution_w, height: _resolution_h, format: "d32 float s8", type: "2d", layers: 1, mip_levels: 1, depth_target: true }) var white_pixels = blob_mod(32, true) _white_texture = create_texture(1, 1, stone(white_pixels)) return true } function get_window() { return _window } function get_gpu() { return _gpu } function get_resolution() { return { width: _resolution_w, height: _resolution_h } } function get_white_texture() { return _white_texture } function get_depth_texture() { return _depth_texture } // Get sampler based on style (0=ps1/saturn nearest, 1=n64 linear) function get_sampler(style_id) { return style_id == 1 ? _sampler_linear : _sampler_nearest } function get_pipeline(skinned, alpha_mode, cull) { var key = `${skinned}_${alpha_mode}_${cull}` if (_pipelines[key]) return _pipelines[key] var blend_enabled = alpha_mode == "blend" var depth_write = alpha_mode != "blend" var blend_config = { enabled: false } if (blend_enabled) { blend_config = { enabled: true, src_rgb: "src_alpha", dst_rgb: "one_minus_src_alpha", op_rgb: "add", src_alpha: "one", dst_alpha: "one_minus_src_alpha", op_alpha: "add" } } var cull_mode = cull == "none" ? "none" : (cull == "front" ? "front" : "back") var vert_shader = skinned ? _skinned_vert_shader : _vert_shader if (!vert_shader) return null var pitch = skinned ? 80 : 48 var vertex_attrs = [ { location: 0, buffer_slot: 0, format: "float3", offset: 0 }, { location: 1, buffer_slot: 0, format: "float3", offset: 12 }, { location: 2, buffer_slot: 0, format: "float2", offset: 24 }, { location: 3, buffer_slot: 0, format: "float4", offset: 32 } ] if (skinned) { vertex_attrs.push({ location: 4, buffer_slot: 0, format: "float4", offset: 48 }) vertex_attrs.push({ location: 5, buffer_slot: 0, format: "float4", offset: 64 }) } var pipeline =gpu_mod.graphics_pipeline(_gpu, { vertex: vert_shader, fragment: _frag_shader, primitive: "triangle", cull: cull_mode, face: "counter_clockwise", fill: "fill", vertex_buffer_descriptions: [{ slot: 0, pitch: pitch, input_rate: "vertex" }], vertex_attributes: vertex_attrs, target: { color_targets: [{ format: _swapchain_format, blend: blend_config }], depth: "d32 float s8" }, depth: { test: true, write: depth_write, compare: "less" } }) _pipelines[key] = pipeline return pipeline } function create_texture(w, h, pixels) { var tex =gpu_mod.texture(_gpu, { width: w, height: h, format: "rgba8", type: "2d", layers: 1, mip_levels: 1, sampler: true }) var size = w * h * 4 var transfer =gpu_mod.transfer_buffer(_gpu, { size: size, usage: "upload" }) transfer.copy_blob(_gpu, pixels) var cmd = _gpu.acquire_cmd_buffer() var copy = cmd.copy_pass() copy.upload_to_texture( { transfer_buffer: transfer, offset: 0, pixels_per_row: w, rows_per_layer: h }, { texture: tex, x: 0, y: 0, z: 0, w: w, h: h, d: 1 }, false ) copy.end() cmd.submit() tex.width = w tex.height = h return tex } function create_vertex_buffer(data) { var size = length(data) / 8 var buffer =gpu_mod.buffer(_gpu, { size: size, vertex: true }) var transfer =gpu_mod.transfer_buffer(_gpu, { size: size, usage: "upload" }) transfer.copy_blob(_gpu, data) var cmd = _gpu.acquire_cmd_buffer() var copy = cmd.copy_pass() copy.upload_to_buffer( { transfer_buffer: transfer, offset: 0 }, { buffer: buffer, offset: 0, size: size }, false ) copy.end() cmd.submit() return buffer } function create_index_buffer(data) { var size = length(data) / 8 var buffer =gpu_mod.buffer(_gpu, { size: size, index: true }) var transfer =gpu_mod.transfer_buffer(_gpu, { size: size, usage: "upload" }) transfer.copy_blob(_gpu, data) var cmd = _gpu.acquire_cmd_buffer() var copy = cmd.copy_pass() copy.upload_to_buffer( { transfer_buffer: transfer, offset: 0 }, { buffer: buffer, offset: 0, size: size }, false ) copy.end() cmd.submit() return buffer } // Submit a frame of draws // draws: array of { mesh, uniforms, texture, coverage, face, palette } // clear_color: [r, g, b, a] // clear_depth: boolean // style_id: 0=ps1, 1=n64, 2=saturn (for sampler selection) function submit_frame(draws, clear_color, clear_depth, style_id) { if (!_gpu) return { draw_calls: 0, triangles: 0 } var cmd = _gpu.acquire_cmd_buffer() var pass_desc = { color_targets: [{ texture: null, load: "clear", store: "store", clear_color: { r: clear_color[0], g: clear_color[1], b: clear_color[2], a: clear_color[3] } }] } if (_depth_texture) { pass_desc.depth_stencil = { texture: _depth_texture, load: clear_depth ? "clear" : "load", store: "dont_care", stencil_load: "clear", stencil_store: "dont_care", clear: 1.0, clear_stencil: 0 } } var swap_tex = cmd.acquire_swapchain_texture(_window) if (!swap_tex) { if (cmd.cancel) cmd.cancel() // Cancel if possible, or just submit empty? // If we can't acquire, we probably shouldn't submit half-baked command buffer. // But cmd is acquired. // Just return. return { draw_calls: 0, triangles: 0 } } pass_desc.color_targets[0].texture = swap_tex var swap_pass = cmd.render_pass(pass_desc) // Sort draws: opaque first, then cutoff, then blend var keys = array(draws, d => { var k = order[d.coverage] return k == null ? 0 : k }) draws = sort(draws, keys) var draw_calls = 0 var triangles = 0 var sampler = get_sampler(style_id) for (var i = 0; i < length(draws); i++) { var d = draws[i] var skinned = d.mesh.skinned && d.palette var cull = d.face == "double" ? "none" : "back" var alpha_mode = d.coverage == "blend" ? "blend" : (d.coverage == "cutoff" || d.coverage == "mask" ? "mask" : "opaque") var pipeline = get_pipeline(skinned, alpha_mode, cull) if (!pipeline) continue swap_pass.bind_pipeline(pipeline) swap_pass.bind_vertex_buffers(0, [{ buffer: d.mesh.vertex_buffer, offset: 0 }]) swap_pass.bind_index_buffer({ buffer: d.mesh.index_buffer, offset: 0 }, d.mesh.index_type == "uint32" ? 32 : 16) cmd.push_vertex_uniform_data(1, d.uniforms) cmd.push_fragment_uniform_data(1, d.uniforms) if (skinned && d.palette) { cmd.push_vertex_uniform_data(2, d.palette) } swap_pass.bind_fragment_samplers(0, [{ texture: d.texture, sampler: sampler }]) swap_pass.draw_indexed(d.mesh.index_count, 1, 0, 0, 0) draw_calls++ triangles += d.mesh.index_count / 3 } swap_pass.end() cmd.submit() return { draw_calls: draw_calls, triangles: triangles } } return { init: init, create_texture: create_texture, create_vertex_buffer: create_vertex_buffer, create_index_buffer: create_index_buffer, submit_frame: submit_frame, get_white_texture }