389 lines
9.2 KiB
Plaintext
389 lines
9.2 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 = 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_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 < 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,
|
|
get_white_texture
|
|
}
|