Files
retro3d/sdl.cm
2025-12-14 01:36:35 -06:00

377 lines
8.9 KiB
Plaintext

// 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 = new video.window({
title: opts.title || "lance3d",
width: _resolution_w,
height: _resolution_h
})
_gpu = new 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 = new gpu_mod.shader(_gpu, {
code: vert_code,
stage: "vertex",
format: "msl",
entrypoint: "vertex_main",
num_uniform_buffers: 2
})
_frag_shader = new 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 = new 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 = new gpu_mod.sampler(_gpu, {
min_filter: "nearest",
mag_filter: "nearest",
u: "repeat",
v: "repeat"
})
_sampler_linear = new gpu_mod.sampler(_gpu, {
min_filter: "linear",
mag_filter: "linear",
u: "repeat",
v: "repeat"
})
_depth_texture = new 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 = new 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 = new 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 = new 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 = new 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 = data.length / 8
var buffer = new gpu_mod.buffer(_gpu, {
size: size,
vertex: true
})
var transfer = new 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 = data.length / 8
var buffer = new gpu_mod.buffer(_gpu, {
size: size,
index: true
})
var transfer = new 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_pass = cmd.swapchain_pass(_window, pass_desc)
// Sort draws: opaque first, then cutoff, then blend
draws.sort(function(a, b) {
var order = { opaque: 0, cutoff: 1, mask: 1, blend: 2 }
return (order[a.coverage] || 0) - (order[b.coverage] || 0)
})
var draw_calls = 0
var triangles = 0
var sampler = get_sampler(style_id)
for (var i = 0; i < draws.length; 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
}