Files
prosperon/sdl_gpu.cm
2026-01-06 20:25:55 -06:00

2004 lines
58 KiB
Plaintext

// sdl_gpu.cm - SDL3 GPU Backend for fx_graph
//
// Direct SDL3 GPU implementation - does NOT use prosperon.cm
// Handles window creation, GPU init, texture loading, and rendering
var video = use('sdl3/video')
var gpu_mod = use('sdl3/gpu')
var blob_mod = use('blob')
var io = use('fd')
var png = use('image/png')
var qoi = use('image/qoi')
var gif = use('image/gif')
var aseprite = use('image/aseprite')
var staef = use('staef')
var res = use('resources')
var geometry = use('geometry')
var sdl_gpu = {}
// Private state
var _gpu = null
var _window = null
var _swapchain_format = null
var _window_width = 1280
var _window_height = 720
// Shaders
var _sprite_vert = null
var _sprite_frag = null
var _blit_vert = null
var _blit_frag = null
var _threshold_frag = null
var _blur_frag = null
var _mask_frag = null
var _crt_frag = null
var _accumulator_frag = null
var _text_sdf_frag = null
var _text_msdf_frag = null
// Pipelines
var _pipelines = {}
// Samplers
var _sampler_nearest = null
var _sampler_linear = null
// Texture cache: path -> {texture, width, height}
var _texture_cache = {}
var _white_texture = null
// Font cache: path.size -> font
var _font_cache = {}
// Render target pool
var _target_pool = {}
// ========================================================================
// INITIALIZATION
// ========================================================================
sdl_gpu.init = function(opts) {
opts = opts || {}
_window_width = opts.width || 1280
_window_height = opts.height || 720
_window = new video.window({
title: opts.title || "Prosperon",
width: _window_width,
height: _window_height,
resizable: true
})
_gpu = new gpu_mod.gpu({debug: true, shaders_msl: true, lowpower: true})
_gpu.claim_window(_window)
_swapchain_format = _gpu.swapchain_format(_window)
// Load shaders
if (!_load_shaders()) {
log.console("sdl_gpu: Failed to load shaders")
return false
}
// Create samplers
_sampler_nearest = new gpu_mod.sampler(_gpu, {
min_filter: "nearest",
mag_filter: "nearest",
u: "clamp_to_edge",
v: "clamp_to_edge"
})
_sampler_linear = new gpu_mod.sampler(_gpu, {
min_filter: "linear",
mag_filter: "linear",
u: "clamp_to_edge",
v: "clamp_to_edge"
})
// Create white texture for untextured draws
var white_pixels = new blob_mod(32, true)
_white_texture = _create_gpu_texture(1, 1, stone(white_pixels))
// Create pipelines
_create_pipelines()
log.console("sdl_gpu: Initialized")
return true
}
sdl_gpu.get_window = function() {
return _window
}
sdl_gpu.get_device = function() {
return _gpu
}
sdl_gpu.set_window_size = function(w, h) {
_window_width = w
_window_height = h
}
sdl_gpu.get_window_size = function() {
return {width: _window_width, height: _window_height}
}
// ========================================================================
// SHADER LOADING
// ========================================================================
function _load_shaders() {
var sprite_vert_code = io.slurp("shaders/msl/sprite2d.vert.msl")
var sprite_frag_code = io.slurp("shaders/msl/sprite2d.frag.msl")
var blit_vert_code = io.slurp("shaders/msl/blit.vert.msl")
var blit_frag_code = io.slurp("shaders/msl/blit.frag.msl")
var threshold_frag_code = io.slurp("shaders/msl/threshold.frag.msl")
var blur_frag_code = io.slurp("shaders/msl/blur.frag.msl")
var mask_frag_code = io.slurp("shaders/msl/mask.frag.msl")
var text_sdf_frag_code = io.slurp("shaders/msl/text_sdf.frag.msl")
if (!sprite_vert_code || !sprite_frag_code) {
log.console("sdl_gpu: Missing sprite shaders")
return false
}
_sprite_vert = new gpu_mod.shader(_gpu, {
code: sprite_vert_code,
stage: "vertex",
format: "msl",
entrypoint: "vertex_main",
num_uniform_buffers: 1
})
_sprite_frag = new gpu_mod.shader(_gpu, {
code: sprite_frag_code,
stage: "fragment",
format: "msl",
entrypoint: "fragment_main",
num_uniform_buffers: 0,
num_samplers: 1
})
if (blit_vert_code && blit_frag_code) {
_blit_vert = new gpu_mod.shader(_gpu, {
code: blit_vert_code,
stage: "vertex",
format: "msl",
entrypoint: "vertex_main",
num_uniform_buffers: 0
})
_blit_frag = new gpu_mod.shader(_gpu, {
code: blit_frag_code,
stage: "fragment",
format: "msl",
entrypoint: "fragment_main",
num_uniform_buffers: 0,
num_samplers: 1
})
}
if (threshold_frag_code) {
_threshold_frag = new gpu_mod.shader(_gpu, {
code: threshold_frag_code,
stage: "fragment",
format: "msl",
entrypoint: "fragment_main",
num_uniform_buffers: 1,
num_samplers: 1
})
}
if (blur_frag_code) {
_blur_frag = new gpu_mod.shader(_gpu, {
code: blur_frag_code,
stage: "fragment",
format: "msl",
entrypoint: "fragment_main",
num_uniform_buffers: 1,
num_samplers: 1
})
}
if (mask_frag_code) {
_mask_frag = new gpu_mod.shader(_gpu, {
code: mask_frag_code,
stage: "fragment",
format: "msl",
entrypoint: "fragment_main",
num_uniform_buffers: 2,
num_samplers: 2
})
}
if (text_sdf_frag_code) {
_text_sdf_frag = new gpu_mod.shader(_gpu, {
code: text_sdf_frag_code,
stage: "fragment",
format: "msl",
entrypoint: "fragment_main",
num_uniform_buffers: 1,
num_samplers: 1
})
}
var text_msdf_frag_code = io.slurp("shaders/msl/text_msdf.frag.msl")
if (text_msdf_frag_code) {
_text_msdf_frag = new gpu_mod.shader(_gpu, {
code: text_msdf_frag_code,
stage: "fragment",
format: "msl",
entrypoint: "fragment_main",
num_uniform_buffers: 1,
num_samplers: 1
})
}
var crt_frag_code = io.slurp("shaders/msl/crt.frag.msl")
if (crt_frag_code) {
_crt_frag = new gpu_mod.shader(_gpu, {
code: crt_frag_code,
stage: "fragment",
format: "msl",
entrypoint: "fragment_main",
num_uniform_buffers: 1,
num_samplers: 1
})
}
var accumulator_frag_code = io.slurp("shaders/msl/accumulator.frag.msl")
if (accumulator_frag_code) {
_accumulator_frag = new gpu_mod.shader(_gpu, {
code: accumulator_frag_code,
stage: "fragment",
format: "msl",
entrypoint: "fragment_main",
num_uniform_buffers: 1,
num_samplers: 2
})
}
return true
}
// ========================================================================
// PIPELINE CREATION
// ========================================================================
function _create_pipelines() {
// Sprite pipeline (alpha blend)
_pipelines.sprite_alpha = new gpu_mod.graphics_pipeline(_gpu, {
vertex: _sprite_vert,
fragment: _sprite_frag,
primitive: "triangle",
cull: "none",
face: "counter_clockwise",
fill: "fill",
vertex_buffer_descriptions: [{
slot: 0,
pitch: 32, // pos(2) + uv(2) + color(4) = 8 floats = 32 bytes
input_rate: "vertex"
}],
vertex_attributes: [
{location: 0, buffer_slot: 0, format: "float2", offset: 0}, // pos
{location: 1, buffer_slot: 0, format: "float2", offset: 8}, // uv
{location: 2, buffer_slot: 0, format: "float4", offset: 16} // color
],
target: {
color_targets: [{
format: _swapchain_format,
blend: {
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"
}
}]
}
})
// Sprite pipeline (additive blend for bloom)
_pipelines.sprite_add = new gpu_mod.graphics_pipeline(_gpu, {
vertex: _sprite_vert,
fragment: _sprite_frag,
primitive: "triangle",
cull: "none",
face: "counter_clockwise",
fill: "fill",
vertex_buffer_descriptions: [{
slot: 0,
pitch: 32,
input_rate: "vertex"
}],
vertex_attributes: [
{location: 0, buffer_slot: 0, format: "float2", offset: 0},
{location: 1, buffer_slot: 0, format: "float2", offset: 8},
{location: 2, buffer_slot: 0, format: "float4", offset: 16}
],
target: {
color_targets: [{
format: _swapchain_format,
blend: {
enabled: true,
src_rgb: "one",
dst_rgb: "one",
op_rgb: "add",
src_alpha: "one",
dst_alpha: "one",
op_alpha: "add"
}
}]
}
})
// Blit pipeline (for fullscreen passes)
if (_blit_vert && _blit_frag) {
_pipelines.blit = new gpu_mod.graphics_pipeline(_gpu, {
vertex: _blit_vert,
fragment: _blit_frag,
primitive: "triangle",
cull: "none",
face: "counter_clockwise",
fill: "fill",
vertex_buffer_descriptions: [{
slot: 0,
pitch: 16, // pos(2) + uv(2) = 4 floats = 16 bytes
input_rate: "vertex"
}],
vertex_attributes: [
{location: 0, buffer_slot: 0, format: "float2", offset: 0},
{location: 1, buffer_slot: 0, format: "float2", offset: 8}
],
target: {
color_targets: [{
format: _swapchain_format,
blend: {
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"
}
}]
}
})
}
// Threshold pipeline (for bloom extraction)
if (_blit_vert && _threshold_frag) {
_pipelines.threshold = new gpu_mod.graphics_pipeline(_gpu, {
vertex: _blit_vert,
fragment: _threshold_frag,
primitive: "triangle",
cull: "none",
face: "counter_clockwise",
fill: "fill",
vertex_buffer_descriptions: [{
slot: 0,
pitch: 16,
input_rate: "vertex"
}],
vertex_attributes: [
{location: 0, buffer_slot: 0, format: "float2", offset: 0},
{location: 1, buffer_slot: 0, format: "float2", offset: 8}
],
target: {
color_targets: [{format: _swapchain_format, blend: {enabled: false}}]
}
})
}
// Blur pipeline
if (_blit_vert && _blur_frag) {
_pipelines.blur = new gpu_mod.graphics_pipeline(_gpu, {
vertex: _blit_vert,
fragment: _blur_frag,
primitive: "triangle",
cull: "none",
face: "counter_clockwise",
fill: "fill",
vertex_buffer_descriptions: [{
slot: 0,
pitch: 16,
input_rate: "vertex"
}],
vertex_attributes: [
{location: 0, buffer_slot: 0, format: "float2", offset: 0},
{location: 1, buffer_slot: 0, format: "float2", offset: 8}
],
target: {
color_targets: [{format: _swapchain_format, blend: {enabled: false}}]
}
})
}
// Mask pipeline
if (_blit_vert && _mask_frag) {
_pipelines.mask = new gpu_mod.graphics_pipeline(_gpu, {
vertex: _blit_vert,
fragment: _mask_frag,
primitive: "triangle",
cull: "none",
face: "counter_clockwise",
fill: "fill",
vertex_buffer_descriptions: [{
slot: 0,
pitch: 16,
input_rate: "vertex"
}],
vertex_attributes: [
{location: 0, buffer_slot: 0, format: "float2", offset: 0},
{location: 1, buffer_slot: 0, format: "float2", offset: 8}
],
target: {
color_targets: [{
format: _swapchain_format,
blend: {
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"
}
}]
}
})
}
// CRT pipeline
if (_blit_vert && _crt_frag) {
_pipelines.crt = new gpu_mod.graphics_pipeline(_gpu, {
vertex: _blit_vert,
fragment: _crt_frag,
primitive: "triangle",
cull: "none",
face: "counter_clockwise",
fill: "fill",
vertex_buffer_descriptions: [{
slot: 0,
pitch: 16,
input_rate: "vertex"
}],
vertex_attributes: [
{location: 0, buffer_slot: 0, format: "float2", offset: 0},
{location: 1, buffer_slot: 0, format: "float2", offset: 8}
],
target: {
color_targets: [{format: _swapchain_format, blend: {enabled: false}}]
}
})
}
// Blit additive pipeline (for bloom compositing)
if (_blit_vert && _blit_frag) {
_pipelines.blit_add = new gpu_mod.graphics_pipeline(_gpu, {
vertex: _blit_vert,
fragment: _blit_frag,
primitive: "triangle",
cull: "none",
face: "counter_clockwise",
fill: "fill",
vertex_buffer_descriptions: [{
slot: 0,
pitch: 16,
input_rate: "vertex"
}],
vertex_attributes: [
{location: 0, buffer_slot: 0, format: "float2", offset: 0},
{location: 1, buffer_slot: 0, format: "float2", offset: 8}
],
target: {
color_targets: [{
format: _swapchain_format,
blend: {
enabled: true,
src_rgb: "one",
dst_rgb: "one",
op_rgb: "add",
src_alpha: "one",
dst_alpha: "one",
op_alpha: "add"
}
}]
}
})
}
// SDF text pipeline
if (_sprite_vert && _text_sdf_frag) {
_pipelines.text_sdf = new gpu_mod.graphics_pipeline(_gpu, {
vertex: _sprite_vert,
fragment: _text_sdf_frag,
primitive: "triangle",
cull: "none",
face: "counter_clockwise",
fill: "fill",
vertex_buffer_descriptions: [{
slot: 0,
pitch: 32,
input_rate: "vertex"
}],
vertex_attributes: [
{location: 0, buffer_slot: 0, format: "float2", offset: 0},
{location: 1, buffer_slot: 0, format: "float2", offset: 8},
{location: 2, buffer_slot: 0, format: "float4", offset: 16}
],
target: {
color_targets: [{
format: _swapchain_format,
blend: {
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"
}
}]
}
})
}
// MSDF text pipeline
if (_sprite_vert && _text_msdf_frag) {
_pipelines.text_msdf = new gpu_mod.graphics_pipeline(_gpu, {
vertex: _sprite_vert,
fragment: _text_msdf_frag,
primitive: "triangle",
cull: "none",
face: "counter_clockwise",
fill: "fill",
vertex_buffer_descriptions: [{
slot: 0,
pitch: 32,
input_rate: "vertex"
}],
vertex_attributes: [
{location: 0, buffer_slot: 0, format: "float2", offset: 0},
{location: 1, buffer_slot: 0, format: "float2", offset: 8},
{location: 2, buffer_slot: 0, format: "float4", offset: 16}
],
target: {
color_targets: [{
format: _swapchain_format,
blend: {
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"
}
}]
}
})
// Accumulator pipeline
if (_blit_vert && _accumulator_frag) {
_pipelines.accumulator = new gpu_mod.graphics_pipeline(_gpu, {
vertex: _blit_vert,
fragment: _accumulator_frag,
primitive: "triangle",
cull: "none",
face: "counter_clockwise",
fill: "fill",
vertex_buffer_descriptions: [{
slot: 0,
pitch: 16,
input_rate: "vertex"
}],
vertex_attributes: [
{location: 0, buffer_slot: 0, format: "float2", offset: 0},
{location: 1, buffer_slot: 0, format: "float2", offset: 8}
],
target: {
color_targets: [{format: _swapchain_format, blend: {enabled: false}}]
}
})
} }
}
// ========================================================================
// TEXTURE MANAGEMENT
// ========================================================================
function _create_gpu_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 _load_image_file(path) {
var bytes = io.slurp(path)
var decoded
if (!bytes) return null
var ext = path.split('.').pop().toLowerCase()
var surface = null
switch (ext) {
case 'png':
case 'jpg':
case 'jpeg':
case 'bmp':
surface = png.decode(bytes)
break
case 'qoi':
surface = qoi.decode(bytes)
break
case 'gif':
decoded = gif.decode(bytes)
if (decoded && decoded.frames && decoded.frames.length > 0) {
surface = decoded.frames[0]
}
break
case 'ase':
case 'aseprite':
decoded = aseprite.decode(bytes)
if (decoded && decoded.frames && decoded.frames.length > 0) {
surface = decoded.frames[0]
}
break
}
return surface
}
sdl_gpu.get_texture = function(path) {
if (!path) return _white_texture
// Check cache
if (_texture_cache[path]) {
return _texture_cache[path]
}
// Find and load image
var fullpath = res.find_image(path)
if (!fullpath) {
log.console(`sdl_gpu: Image not found: ${path}`)
return _white_texture
}
var surface = _load_image_file(fullpath)
if (!surface || !surface.pixels) {
log.console(`sdl_gpu: Failed to load image: ${path}`)
return _white_texture
}
var tex = _create_gpu_texture(surface.width, surface.height, surface.pixels)
_texture_cache[path] = tex
return tex
}
// ========================================================================
// RENDER TARGET MANAGEMENT
// ========================================================================
sdl_gpu.get_or_create_target = function(width, height, key) {
// Clamp dimensions to minimum 1x1 to prevent GPU errors
if (!width || width < 1) width = 1
if (!height || height < 1) height = 1
var pool_key = `${width}x${height}`
if (!_target_pool[pool_key])
_target_pool[pool_key] = []
// Reuse from pool if available
// 1. Check if a target with this exact key already exists
if (key) {
for (var target of _target_pool[pool_key]) {
if (target.key == key) {
target.in_use = true
return target
}
}
}
// 2. Otherwise prefer most recently used (LIFO) or just first available
for (var target of _target_pool[pool_key]) {
if (!target.in_use) {
target.in_use = true
target.key = key
return target
}
}
// Create new render target texture
var tex = new gpu_mod.texture(_gpu, {
width: width,
height: height,
format: _swapchain_format,
type: "2d",
layers: 1,
mip_levels: 1,
sampler: true,
color_target: true
})
tex.width = width
tex.height = height
var target = {
texture: tex,
width: width,
height: height,
in_use: true,
key: key
}
_target_pool[pool_key].push(target)
return target
}
sdl_gpu.release_all_targets = function() {
for (var pool_key in _target_pool) {
for (var target of _target_pool[pool_key])
target.in_use = false
}
}
// ========================================================================
// GEOMETRY BUILDING
// ========================================================================
// Build vertex data for sprites
// Vertex format: pos(2) + uv(2) + color(4) = 8 floats = 32 bytes
function _build_sprite_vertices(sprites, camera) {
var floats_per_vertex = 8
var vertices_per_sprite = 4
var indices_per_sprite = 6
var vertex_data = new blob_mod(sprites.length * vertices_per_sprite * floats_per_vertex * 4)
var index_data = new blob_mod(sprites.length * indices_per_sprite * 2)
var vertex_count = 0
var white = {r: 1, g: 1, b: 1, a: 1}
array.for(sprites, s => {
var px = s.pos.x
var py = s.pos.y
var w = s.width || 1
var h = s.height || 1
var ax = s.anchor_x || 0
var ay = s.anchor_y || 0
var c = s.color || white
// Apply anchor
var x = px - w * ax
var y = py - h * ay
// UV coordinates (handle sprite rect if present)
var u0 = s.uv_rect ? s.uv_rect.x : 0
var v0 = s.uv_rect ? s.uv_rect.y : 0
var u1 = s.uv_rect ? (s.uv_rect.x + s.uv_rect.width) : 1
var v1 = s.uv_rect ? (s.uv_rect.y + s.uv_rect.height) : 1
// Quad vertices (bottom-left, bottom-right, top-right, top-left)
// v0: bottom-left
vertex_data.wf(x)
vertex_data.wf(y)
vertex_data.wf(u0)
vertex_data.wf(v1) // Flip V
vertex_data.wf(c.r)
vertex_data.wf(c.g)
vertex_data.wf(c.b)
vertex_data.wf(c.a)
// v1: bottom-right
vertex_data.wf(x + w)
vertex_data.wf(y)
vertex_data.wf(u1)
vertex_data.wf(v1) // Flip V
vertex_data.wf(c.r)
vertex_data.wf(c.g)
vertex_data.wf(c.b)
vertex_data.wf(c.a)
// v2: top-right
vertex_data.wf(x + w)
vertex_data.wf(y + h)
vertex_data.wf(u1)
vertex_data.wf(v0) // Flip V
vertex_data.wf(c.r)
vertex_data.wf(c.g)
vertex_data.wf(c.b)
vertex_data.wf(c.a)
// v3: top-left
vertex_data.wf(x)
vertex_data.wf(y + h)
vertex_data.wf(u0)
vertex_data.wf(v0) // Flip V
vertex_data.wf(c.r)
vertex_data.wf(c.g)
vertex_data.wf(c.b)
vertex_data.wf(c.a)
// Indices (two triangles)
index_data.w16(vertex_count + 0)
index_data.w16(vertex_count + 1)
index_data.w16(vertex_count + 2)
index_data.w16(vertex_count + 0)
index_data.w16(vertex_count + 2)
index_data.w16(vertex_count + 3)
vertex_count += 4
})
return {
vertices: stone(vertex_data),
indices: stone(index_data),
vertex_count: vertex_count,
index_count: sprites.length * 6
}
}
// Build fullscreen quad for blit/post-processing
function _build_fullscreen_quad(dst_rect, target_width, target_height) {
// Convert pixel rect to NDC
var x0 = (dst_rect.x / target_width) * 2 - 1
var y0 = (dst_rect.y / target_height) * 2 - 1
var x1 = ((dst_rect.x + dst_rect.width) / target_width) * 2 - 1
var y1 = ((dst_rect.y + dst_rect.height) / target_height) * 2 - 1
var vertex_data = new blob_mod(4 * 4 * 4) // 4 verts * 4 floats * 4 bytes
var index_data = new blob_mod(6 * 2) // 6 indices * 2 bytes
// Metal textures have origin at top-left (uv 0,0 = top-left of texture)
// NDC has origin at center (y=-1 is bottom, y=1 is top)
// So we need to flip V: screen bottom (y0) samples texture bottom (v=1)
// screen top (y1) samples texture top (v=0)
// v0: bottom-left (NDC) -> sample texture bottom-left (u=0, v=1)
vertex_data.wf(x0);
vertex_data.wf(y0);
vertex_data.wf(0);
vertex_data.wf(1); // v=1 (bottom of texture)
// v1: bottom-right (NDC) -> sample texture bottom-right (u=1, v=1)
vertex_data.wf(x1);
vertex_data.wf(y0);
vertex_data.wf(1);
vertex_data.wf(1); // v=1 (bottom of texture)
// v2: top-right (NDC) -> sample texture top-right (u=1, v=0)
vertex_data.wf(x1);
vertex_data.wf(y1);
vertex_data.wf(1);
vertex_data.wf(0); // v=0 (top of texture)
// v3: top-left (NDC) -> sample texture top-left (u=0, v=0)
vertex_data.wf(x0);
vertex_data.wf(y1);
vertex_data.wf(0);
vertex_data.wf(0); // v=0 (top of texture)
index_data.w16(0)
index_data.w16(1)
index_data.w16(2)
index_data.w16(0)
index_data.w16(2)
index_data.w16(3)
return {
vertices: stone(vertex_data),
indices: stone(index_data),
vertex_count: 4,
index_count: 6
}
}
// ========================================================================
// MATRIX BUILDING
// ========================================================================
function _build_ortho_matrix(left, right, bottom, top, near, far) {
var data = new blob_mod(64)
var m = []
m[0] = 2 / (right - left)
m[1] = 0
m[2] = 0
m[3] = 0
m[4] = 0
m[5] = 2 / (top - bottom)
m[6] = 0
m[7] = 0
m[8] = 0
m[9] = 0
m[10] = -2 / (far - near)
m[11] = 0
m[12] = -(right + left) / (right - left)
m[13] = -(top + bottom) / (top - bottom)
m[14] = -(far + near) / (far - near)
m[15] = 1
for (var i = 0; i < 16; i++)
data.wf(m[i])
return stone(data)
}
function _build_camera_matrix(camera, target_width, target_height) {
var pos = camera.pos || {x: 0, y: 0}
var cam_width = camera.width || target_width
var cam_height = camera.height || target_height
var anchor = camera.anchor || {x: 0.5, y: 0.5}
var left = pos.x - cam_width * anchor.x
var right = pos.x + cam_width * (1 - anchor.x)
var bottom = pos.y - cam_height * anchor.y
var top = pos.y + cam_height * (1 - anchor.y)
return _build_ortho_matrix(left, right, bottom, top, -1, 1)
}
// ========================================================================
// GRAPH EXECUTION
// ========================================================================
var ex = 0
sdl_gpu.execute_graph = function(graph, window_size, dbg = false) {
_window_width = window_size.width
_window_height = window_size.height
// Execute graph to get all commands
var result = graph.execute(this)
if (dbg) {
log.console(result)
return
}
var all_commands = result.commands || []
// Pre-load all textures needed
_preload_textures(all_commands)
// Execute commands
_execute_commands(all_commands, window_size)
// Release targets for next frame
this.release_all_targets()
}
// Execute commands directly (from compositor)
sdl_gpu.execute_commands = function(commands, window_size, dbg = false) {
_window_width = window_size.width
_window_height = window_size.height
if (dbg) {
log.console(commands)
return
}
// Pre-load all textures needed
_preload_textures(commands)
// Execute commands
_execute_commands(commands, window_size)
// Release targets for next frame
this.release_all_targets()
}
function _preload_textures(commands) {
var paths = {}
for (var cmd of commands) {
if (cmd.cmd == 'draw_batch' && cmd.texture) {
if (is_text(cmd.texture)) {
paths[cmd.texture] = true
}
}
}
// Load all textures
for (var path in paths) {
sdl_gpu.get_texture(path)
}
}
function _execute_commands(commands, window_size) {
var cmd_buffer = _gpu.acquire_cmd_buffer()
var current_pass = null
var current_target = null
var current_camera = null
var pending_draws = []
var target
// Cache swapchain texture for the duration of this command buffer
var _swapchain_tex = null
function get_swapchain_tex() {
if (_swapchain_tex) return _swapchain_tex
_swapchain_tex = cmd_buffer.acquire_swapchain_texture(_window)
return _swapchain_tex
}
for (var cmd of commands) {
switch (cmd.cmd) {
case 'begin_render':
// Flush pending draws
if (current_pass && pending_draws.length > 0) {
_flush_draws(cmd_buffer, current_pass, pending_draws, current_camera, current_target)
pending_draws = []
}
// End previous pass
if (current_pass) {
current_pass.end()
current_pass = null
}
// Start new pass
target = cmd.target
var clear = cmd.clear
if (target == 'screen') {
var swap_tex = get_swapchain_tex()
if (swap_tex) {
current_pass = cmd_buffer.render_pass({
color_targets: [{
texture: swap_tex,
load: clear ? "clear" : "load",
store: "store",
clear_color: clear ? {r: clear.r, g: clear.g, b: clear.b, a: clear.a} : {r: 0, g: 0, b: 0, a: 0}
}]
})
current_target = {texture: swap_tex, width: swap_tex.width, height: swap_tex.height}
} else {
log.console("sdl_gpu: Failed to acquire swapchain texture")
current_pass = null
current_target = window_size
}
} else {
current_pass = cmd_buffer.render_pass({
color_targets: [{
texture: target.texture,
load: clear ? "clear" : "load",
store: "store",
clear_color: clear ? {r: clear.r, g: clear.g, b: clear.b, a: clear.a} : {r: 0, g: 0, b: 0, a: 0}
}]
})
current_target = target
}
break
case 'set_camera':
current_camera = cmd.camera
break
case 'draw_batch':
pending_draws.push(cmd)
break
case 'draw_text':
pending_draws.push(cmd)
break
case 'draw_texture_ref':
pending_draws.push(cmd)
break
case 'blit':
// Flush pending draws first
if (current_pass && pending_draws.length > 0) {
_flush_draws(cmd_buffer, current_pass, pending_draws, current_camera, current_target)
pending_draws = []
}
// End current pass - SDL blit works outside render passes
if (current_pass) {
current_pass.end()
current_pass = null
}
_do_blit(cmd_buffer, cmd, current_target, get_swapchain_tex)
break
case 'apply_mask':
// Flush pending draws first
if (current_pass && pending_draws.length > 0) {
_flush_draws(cmd_buffer, current_pass, pending_draws, current_camera, current_target)
pending_draws = []
}
// End current pass - mask works as blit outside render pass
if (current_pass) {
current_pass.end()
current_pass = null
}
_do_mask(cmd_buffer, cmd)
break
case 'shader_pass':
// Flush pending draws first
if (current_pass && pending_draws.length > 0) {
_flush_draws(cmd_buffer, current_pass, pending_draws, current_camera, current_target)
pending_draws = []
}
// End current pass - shader passes need their own render pass
if (current_pass) {
current_pass.end()
current_pass = null
}
_do_shader_pass(cmd_buffer, cmd, get_swapchain_tex)
break
case 'composite_textures':
// Flush pending draws first
if (current_pass && pending_draws.length > 0) {
_flush_draws(cmd_buffer, current_pass, pending_draws, current_camera, current_target)
pending_draws = []
}
// End current pass
if (current_pass) {
current_pass.end()
current_pass = null
}
_do_composite(cmd_buffer, cmd, window_size)
break
case 'end_render':
// Flush pending draws
if (current_pass && pending_draws.length > 0) {
_flush_draws(cmd_buffer, current_pass, pending_draws, current_camera, current_target)
pending_draws = []
}
if (current_pass) {
current_pass.end()
current_pass = null
}
break
case 'imgui':
// Flush pending draws first
if (current_pass && pending_draws.length > 0) {
_flush_draws(cmd_buffer, current_pass, pending_draws, current_camera, current_target)
pending_draws = []
}
// ImGui needs to be outside a render pass for prepare, but inside for endframe
if (current_pass) {
current_pass.end()
current_pass = null
}
var imgui_mod = use('imgui')
if (cmd.draw) {
cmd.draw(imgui_mod)
}
imgui_mod.prepare(cmd_buffer)
// Restart pass to the same target for rendering
target = cmd.target
var swap_tex = null
if (target == 'screen') {
swap_tex = get_swapchain_tex()
if (swap_tex) {
current_pass = cmd_buffer.render_pass({
color_targets: [{
texture: swap_tex,
load: "load",
store: "store"
}]
})
}
} else if (target && target.texture) {
current_pass = cmd_buffer.render_pass({
color_targets: [{
texture: target.texture,
load: "load",
store: "store"
}]
})
}
if (current_pass) {
imgui_mod.endframe(cmd_buffer, current_pass)
current_pass.end()
current_pass = null
}
break
case 'present':
// Submit command buffer
break
}
}
// Final flush
if (current_pass && pending_draws.length > 0) {
_flush_draws(cmd_buffer, current_pass, pending_draws, current_camera, current_target)
}
if (current_pass) {
current_pass.end()
}
cmd_buffer.submit()
}
function _flush_draws(cmd_buffer, pass, draws, camera, target) {
var current_batch = null
// Iterate draws preserving order
for (var draw of draws) {
if (draw.cmd == 'draw_batch') {
// Sprite batch handling
var tex_path = draw.texture || '_white'
var blend = draw.material ? draw.material.blend : 'alpha'
// Check if we can append to current batch
if (current_batch &&
current_batch.type == 'sprites' &&
current_batch.texture_path == tex_path &&
current_batch.blend == blend) {
// Append sprites
if (draw.geometry && draw.geometry.sprites) {
for (var s of draw.geometry.sprites) {
current_batch.sprites.push(s)
}
}
} else {
// Flush current batch
if (current_batch) _render_batch(cmd_buffer, pass, current_batch, camera, target)
// Start new sprite batch
current_batch = {
type: 'sprites',
texture_path: tex_path,
blend: blend,
sprites: []
}
if (draw.geometry && draw.geometry.sprites) {
for (var s of draw.geometry.sprites) {
current_batch.sprites.push(s)
}
}
}
} else if (draw.cmd == 'draw_text') {
// Flush current batch
if (current_batch) _render_batch(cmd_buffer, pass, current_batch, camera, target)
current_batch = null
// Render text immediately
_render_text(cmd_buffer, pass, draw.drawable, camera, target)
} else if (draw.cmd == 'draw_texture_ref') {
// Flush current batch
if (current_batch) _render_batch(cmd_buffer, pass, current_batch, camera, target)
current_batch = null
// Render pre-rendered effect texture
_render_texture_ref(cmd_buffer, pass, draw.drawable, camera, target)
}
}
// Flush final batch
if (current_batch) _render_batch(cmd_buffer, pass, current_batch, camera, target)
}
function _render_batch(cmd_buffer, pass, batch, camera, target) {
if (batch.type == 'sprites') {
if (batch.sprites.length == 0) return
var tex = batch.texture_path == '_white' ? _white_texture : sdl_gpu.get_texture(batch.texture_path)
var geom = _build_sprite_vertices(batch.sprites, camera)
// Upload geometry
var vb_size = geom.vertices.length / 8
var ib_size = geom.indices.length / 8
var vb = new gpu_mod.buffer(_gpu, {size: vb_size, vertex: true})
var ib = new gpu_mod.buffer(_gpu, {size: ib_size, index: true})
var vb_transfer = new gpu_mod.transfer_buffer(_gpu, {size: vb_size, usage: "upload"})
var ib_transfer = new gpu_mod.transfer_buffer(_gpu, {size: ib_size, usage: "upload"})
vb_transfer.copy_blob(_gpu, geom.vertices)
ib_transfer.copy_blob(_gpu, geom.indices)
var copy_cmd = _gpu.acquire_cmd_buffer()
var copy = copy_cmd.copy_pass()
copy.upload_to_buffer({transfer_buffer: vb_transfer, offset: 0}, {buffer: vb, offset: 0, size: vb_size}, false)
copy.upload_to_buffer({transfer_buffer: ib_transfer, offset: 0}, {buffer: ib, offset: 0, size: ib_size}, false)
copy.end()
copy_cmd.submit()
// Build camera matrix
var proj = _build_camera_matrix(camera, target.width, target.height)
// Select pipeline
var pipeline = batch.blend == 'add' ? _pipelines.sprite_add : _pipelines.sprite_alpha
// Draw
pass.bind_pipeline(pipeline)
pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}])
pass.bind_index_buffer({buffer: ib, offset: 0}, 16)
pass.bind_fragment_samplers(0, [{texture: tex, sampler: _sampler_nearest}])
cmd_buffer.push_vertex_uniform_data(0, proj)
pass.draw_indexed(geom.index_count, 1, 0, 0, 0)
}
}
// Render a pre-rendered texture from an effect group
function _render_texture_ref(cmd_buffer, pass, drawable, camera, target) {
var tex_target = drawable.texture_target
if (!tex_target) return
// The texture_target is a compositor target reference - resolve it
// It should have already been rendered to and we just need to blit it
var pos = drawable.pos || {x: 0, y: 0}
var width = drawable.width || target.width
var height = drawable.height || target.height
// Build a single sprite for the texture reference
var sprites = [{
pos: pos,
width: width,
height: height,
anchor_x: 0,
anchor_y: 0,
color: {r: 1, g: 1, b: 1, a: 1}
}]
var geom = _build_sprite_vertices(sprites, camera)
// Upload geometry
var vb_size = geom.vertices.length / 8
var ib_size = geom.indices.length / 8
var vb = new gpu_mod.buffer(_gpu, {size: vb_size, vertex: true})
var ib = new gpu_mod.buffer(_gpu, {size: ib_size, index: true})
var vb_transfer = new gpu_mod.transfer_buffer(_gpu, {size: vb_size, usage: "upload"})
var ib_transfer = new gpu_mod.transfer_buffer(_gpu, {size: ib_size, usage: "upload"})
vb_transfer.copy_blob(_gpu, geom.vertices)
ib_transfer.copy_blob(_gpu, geom.indices)
var copy_cmd = _gpu.acquire_cmd_buffer()
var copy = copy_cmd.copy_pass()
copy.upload_to_buffer({transfer_buffer: vb_transfer, offset: 0}, {buffer: vb, offset: 0, size: vb_size}, false)
copy.upload_to_buffer({transfer_buffer: ib_transfer, offset: 0}, {buffer: ib, offset: 0, size: ib_size}, false)
copy.end()
copy_cmd.submit()
// Build camera matrix
var proj = _build_camera_matrix(camera, target.width, target.height)
// Select pipeline based on blend mode
var blend = drawable.blend || 'over'
var pipeline = blend == 'add' ? _pipelines.sprite_add : _pipelines.sprite_alpha
// The texture_target has a .texture property from the target pool
var tex = tex_target.texture || tex_target
if (!tex) return
pass.bind_pipeline(pipeline)
pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}])
pass.bind_index_buffer({buffer: ib, offset: 0}, 16)
pass.bind_fragment_samplers(0, [{texture: tex, sampler: _sampler_linear}])
cmd_buffer.push_vertex_uniform_data(0, proj)
pass.draw_indexed(geom.index_count, 1, 0, 0, 0)
}
function _render_text(cmd_buffer, pass, drawable, camera, target) {
// Get font - support mode tag: 'bitmap', 'sdf', 'msdf'
var font_path = drawable.font
var size = drawable.size || 16
var mode = drawable.mode || (drawable.sdf ? 'sdf' : 'bitmap')
var font = _get_font_cache(font_path, size, mode)
if (!font) return
// Generate vertices using staef
var pos = drawable.pos
var text_pos = {x: pos.x, y: pos.y, width: 0, height: 0}
var color = drawable.color || {r:1, g:1, b:1, a:1}
// Handle anchor
var ax = drawable.anchor_x || 0
var ay = drawable.anchor_y || 0
if (ax != 0 || ay != 0) {
var dim = font.text_size(drawable.text)
if (dim) {
text_pos.x -= dim.x * ax
text_pos.y -= dim.y * ay
}
}
var mesh = font.make_text_buffer(drawable.text, text_pos, color)
if (!mesh || !mesh.num_vertices) return
// Interlace buffers manually
var num_verts = mesh.num_vertices
var interleaved = geometry.weave([{data:mesh.xy, stride: mesh.xy_stride}, {data:mesh.uv, stride: mesh.uv_stride}, {data:mesh.color, stride: mesh.color_stride}])
var indices = mesh.indices
var num_indices = mesh.num_indices
// Upload
var vb_size = num_verts * 32
var ib_size = num_indices * 2
var vb = new gpu_mod.buffer(_gpu, {size: vb_size, vertex: true})
var ib = new gpu_mod.buffer(_gpu, {size: ib_size, index: true})
var vb_transfer = new gpu_mod.transfer_buffer(_gpu, {size: vb_size, usage: "upload"})
var ib_transfer = new gpu_mod.transfer_buffer(_gpu, {size: ib_size, usage: "upload"})
vb_transfer.copy_blob(_gpu, interleaved)
ib_transfer.copy_blob(_gpu, indices)
var copy_cmd = _gpu.acquire_cmd_buffer()
var copy = copy_cmd.copy_pass()
copy.upload_to_buffer({transfer_buffer: vb_transfer, offset: 0}, {buffer: vb, offset: 0, size: vb_size}, false)
copy.upload_to_buffer({transfer_buffer: ib_transfer, offset: 0}, {buffer: ib, offset: 0, size: ib_size}, false)
copy.end()
copy_cmd.submit()
// Setup pipeline
var proj = _build_camera_matrix(camera, target.width, target.height)
// Select pipeline based on mode
var is_sdf = (mode == 'sdf')
var is_msdf = (mode == 'msdf')
if (is_msdf && _pipelines.text_msdf) {
pass.bind_pipeline(_pipelines.text_msdf)
// Build uniforms for MSDF
// Struct: float outline_width, float sharpness, float2 _pad, float4 outline_color
var u_data = new blob_mod(32)
// Convert outline_width from pixel-ish units to normalized SDF units
// outline_width in drawable is in "visual" units, we need to normalize
// A typical range is 0.0-0.3 in SDF units
var outline_w = drawable.outline_width || 0
if (outline_w > 0) outline_w = outline_w / 100.0 // Scale down from user units
u_data.wf(outline_w) // outline_width
u_data.wf(font.sharpness || 1.0) // sharpness from font
u_data.wf(0) // _pad.x
u_data.wf(0) // _pad.y
var oc = drawable.outline_color || {r:0, g:0, b:0, a:1}
u_data.wf(oc.r) // outline_color.r
u_data.wf(oc.g) // outline_color.g
u_data.wf(oc.b) // outline_color.b
u_data.wf(oc.a || 1) // outline_color.a
cmd_buffer.push_fragment_uniform_data(0, stone(u_data))
} else if (is_sdf && _pipelines.text_sdf) {
pass.bind_pipeline(_pipelines.text_sdf)
// Build uniforms for SDF
// Struct: float outline_width, float sharpness, float2 _pad, float4 outline_color
var u_data = new blob_mod(32)
var outline_w = drawable.outline_width || 0
if (outline_w > 0) outline_w = outline_w / 100.0
u_data.wf(outline_w) // outline_width
u_data.wf(font.sharpness || 1.0) // sharpness from font
u_data.wf(0) // _pad.x
u_data.wf(0) // _pad.y
var oc = drawable.outline_color || {r:0, g:0, b:0, a:1}
u_data.wf(oc.r)
u_data.wf(oc.g)
u_data.wf(oc.b)
u_data.wf(oc.a || 1)
cmd_buffer.push_fragment_uniform_data(0, stone(u_data))
} else {
pass.bind_pipeline(_pipelines.sprite_alpha)
}
pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}])
pass.bind_index_buffer({buffer: ib, offset: 0}, 16)
// Bind font texture - use linear filtering for SDF/MSDF
var font_tex = _get_font_texture(font, mode)
var sampler = (is_sdf || is_msdf) ? _sampler_linear : _sampler_nearest
pass.bind_fragment_samplers(0, [{texture: font_tex, sampler: sampler}])
cmd_buffer.push_vertex_uniform_data(0, proj)
pass.draw_indexed(num_indices, 1, 0, 0, 0)
}
function _get_font_cache(path, size, mode) {
// mode can be 'bitmap', 'sdf', 'msdf', or boolean (legacy)
if (mode == true) mode = 'sdf'
else if (mode == false || !mode) mode = 'bitmap'
var key = `${path}.${size}.${mode}`
if (_font_cache[key]) return _font_cache[key]
var fullpath = res.find_font(path)
if (!fullpath) return null
var data = io.slurp(fullpath)
if (!data) return null
// Create staef font based on mode
try {
var font
if (mode == 'msdf') {
// MSDF: em_px=size, range_px=4, padding_px=6, sharpness=1.0
font = new staef.msdf_font(data, size, 4.0, 6, 1.0)
} else if (mode == 'sdf') {
// SDF: em_px=size, range_px=12, padding_px=14, sharpness=1.0
font = new staef.sdf_font(data, size, 12.0, 14, 1.0)
} else {
// Bitmap
font = new staef.font(data, size, false)
}
_font_cache[key] = font
return font
} catch(e) {
log.console(`sdl_gpu: Failed to load font ${path}:${size}:${mode}: ${e.message}`)
return null
}
}
function _get_font_texture(font, is_sdf) {
if (font._gpu_texture) return font._gpu_texture
// Create texture from font.texture (pixels, width, height)
var ftex = font.texture
if (!ftex) return _white_texture
// Use linear filtering for SDF? The tex creation just sets format, sampler state is in pipeline/bind.
// We can reuse creation logic.
var tex = _create_gpu_texture(ftex.width, ftex.height, ftex.pixels)
font._gpu_texture = tex
return tex
}
function _do_blit(cmd_buffer, cmd, current_target, get_swapchain_tex) {
var src = cmd.texture
var dst_rect = cmd.dst_rect
var filter = cmd.filter || 'nearest'
var target = cmd.target || current_target
if (!src || !src.texture) return
if (!target) return
if (target == 'screen' || (!target.texture && target.width)) {
// Cannot use SDL_BlitGPUTexture for screen/swapchain, must use render pass
var swap_tex = (target == 'screen') ? get_swapchain_tex() : target.texture
if (!swap_tex && target == 'screen') swap_tex = get_swapchain_tex()
if (!swap_tex) return
var pass = cmd_buffer.render_pass({
color_targets: [{
texture: swap_tex,
load: "clear",
store: "store",
clear_color: {r: 0, g: 0, b: 0, a: 1}
}]
})
var win_size = sdl_gpu.get_window_size()
var geom = _build_fullscreen_quad(dst_rect, win_size.width, win_size.height)
_draw_textured_quad(pass, geom, src.texture, _pipelines.blit, filter)
pass.end()
} else {
// Use render pass with alpha blending instead of SDL blit (which overwrites)
if (!target || !target.texture) return
var geom = _build_fullscreen_quad(dst_rect, target.width, target.height)
var pass = cmd_buffer.render_pass({
color_targets: [{
texture: target.texture,
load: "load", // IMPORTANT: Load existing content so we blend on top
store: "store"
}]
})
_draw_textured_quad(pass, geom, src.texture, _pipelines.blit, filter)
pass.end()
}
}
function _draw_textured_quad(pass, geom, texture, pipeline, filter) {
var vb_size = geom.vertices.length / 8
var ib_size = geom.indices.length / 8
var vb = new gpu_mod.buffer(_gpu, {size: vb_size, vertex: true})
var ib = new gpu_mod.buffer(_gpu, {size: ib_size, index: true})
var vb_transfer = new gpu_mod.transfer_buffer(_gpu, {size: vb_size, usage: "upload"})
var ib_transfer = new gpu_mod.transfer_buffer(_gpu, {size: ib_size, usage: "upload"})
vb_transfer.copy_blob(_gpu, geom.vertices)
ib_transfer.copy_blob(_gpu, geom.indices)
var copy_cmd = _gpu.acquire_cmd_buffer()
var copy = copy_cmd.copy_pass()
copy.upload_to_buffer({transfer_buffer: vb_transfer, offset: 0}, {buffer: vb, offset: 0, size: vb_size}, false)
copy.upload_to_buffer({transfer_buffer: ib_transfer, offset: 0}, {buffer: ib, offset: 0, size: ib_size}, false)
copy.end()
copy_cmd.submit()
pass.bind_pipeline(pipeline)
pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}])
pass.bind_index_buffer({buffer: ib, offset: 0}, 16)
var sampler = filter == 'linear' ? _sampler_linear : _sampler_nearest
pass.bind_fragment_samplers(0, [{texture: texture, sampler: sampler}])
pass.draw_indexed(6, 1, 0, 0, 0)
}
function _do_mask(cmd_buffer, cmd) {
var content = cmd.content_texture
var mask = cmd.mask_texture
var output = cmd.output
var mode = cmd.mode || 'alpha'
var invert = cmd.invert || false
if (!content || !content.texture) return
if (!mask || !mask.texture) return
if (!output || !output.texture) return
// Check if mask pipeline is available
if (!_pipelines.mask) {
log.console("sdl_gpu: Mask pipeline not available, falling back to blit")
cmd_buffer.blit({
src: {texture: content.texture, x: 0, y: 0, width: content.width, height: content.height},
dst: {texture: output.texture, x: 0, y: 0, width: output.width, height: output.height},
load: "clear",
filter: "nearest"
})
return
}
// Build fullscreen quad
var geom = _build_fullscreen_quad({x: 0, y: 0, width: output.width, height: output.height}, output.width, output.height)
var vb_size = geom.vertices.length / 8
var ib_size = geom.indices.length / 8
var vb = new gpu_mod.buffer(_gpu, {size: vb_size, vertex: true})
var ib = new gpu_mod.buffer(_gpu, {size: ib_size, index: true})
var vb_transfer = new gpu_mod.transfer_buffer(_gpu, {size: vb_size, usage: "upload"})
var ib_transfer = new gpu_mod.transfer_buffer(_gpu, {size: ib_size, usage: "upload"})
vb_transfer.copy_blob(_gpu, geom.vertices)
ib_transfer.copy_blob(_gpu, geom.indices)
var copy_cmd = _gpu.acquire_cmd_buffer()
var copy = copy_cmd.copy_pass()
copy.upload_to_buffer({transfer_buffer: vb_transfer, offset: 0}, {buffer: vb, offset: 0, size: vb_size}, false)
copy.upload_to_buffer({transfer_buffer: ib_transfer, offset: 0}, {buffer: ib, offset: 0, size: ib_size}, false)
copy.end()
copy_cmd.submit()
// Build uniforms: invert, mode
var uniform_data = new blob_mod(16)
uniform_data.wf(invert ? 1.0 : 0.0) // invert
uniform_data.wf(mode == 'binary' ? 1.0 : 0.0) // mode (0=alpha, 1=binary)
uniform_data.wf(0) // padding
uniform_data.wf(0) // padding
// Render to output
var mask_pass = cmd_buffer.render_pass({
color_targets: [{
texture: output.texture,
load: "clear",
store: "store",
clear_color: {r: 0, g: 0, b: 0, a: 0}
}]
})
mask_pass.bind_pipeline(_pipelines.mask)
mask_pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}])
mask_pass.bind_index_buffer({buffer: ib, offset: 0}, 16)
// Bind both content texture (slot 0) and mask texture (slot 1)
mask_pass.bind_fragment_samplers(0, [
{texture: content.texture, sampler: _sampler_nearest},
{texture: mask.texture, sampler: _sampler_nearest}
])
cmd_buffer.push_fragment_uniform_data(0, stone(uniform_data))
mask_pass.draw_indexed(6, 1, 0, 0, 0)
mask_pass.end()
}
function _do_shader_pass(cmd_buffer, cmd, get_swapchain_tex) {
var shader = cmd.shader
var input = cmd.input
var output = cmd.output
var uniforms = cmd.uniforms || {}
if (!input || !input.texture) return
if (output != 'screen' && (!output || !output.texture)) return
// Select pipeline based on shader type
var pipeline = null
switch (shader) {
case 'threshold':
pipeline = _pipelines.threshold
break
case 'blur':
pipeline = _pipelines.blur
break
case 'crt':
pipeline = _pipelines.crt
break
case 'accumulator':
pipeline = _pipelines.accumulator
break
case 'mask':
pipeline = _pipelines.mask
break
default:
log.console(`sdl_gpu: Unknown shader: ${shader}`)
return
}
if (!pipeline) {
log.console(`sdl_gpu: Pipeline not available for shader: ${shader}`)
return
}
// Build fullscreen quad
var out_w = output == 'screen' ? _window_width : output.width
var out_h = output == 'screen' ? _window_height : output.height
var geom = _build_fullscreen_quad({x: 0, y: 0, width: out_w, height: out_h}, out_w, out_h)
// Upload geometry
var vb_size = geom.vertices.length / 8
var ib_size = geom.indices.length / 8
var vb = new gpu_mod.buffer(_gpu, {size: vb_size, vertex: true})
var ib = new gpu_mod.buffer(_gpu, {size: ib_size, index: true})
var vb_transfer = new gpu_mod.transfer_buffer(_gpu, {size: vb_size, usage: "upload"})
var ib_transfer = new gpu_mod.transfer_buffer(_gpu, {size: ib_size, usage: "upload"})
vb_transfer.copy_blob(_gpu, geom.vertices)
ib_transfer.copy_blob(_gpu, geom.indices)
var copy_cmd = _gpu.acquire_cmd_buffer()
var copy = copy_cmd.copy_pass()
copy.upload_to_buffer({transfer_buffer: vb_transfer, offset: 0}, {buffer: vb, offset: 0, size: vb_size}, false)
copy.upload_to_buffer({transfer_buffer: ib_transfer, offset: 0}, {buffer: ib, offset: 0, size: ib_size}, false)
copy.end()
copy_cmd.submit()
// Build uniform buffer based on shader type
var uniform_data = _build_shader_uniforms(shader, uniforms)
// Start render pass to output target
var pass
if (output == 'screen') {
var swap_tex = get_swapchain_tex()
if (swap_tex) {
pass = cmd_buffer.render_pass({
color_targets: [{
texture: swap_tex,
load: "clear",
store: "store",
clear_color: {r: 0, g: 0, b: 0, a: 1}
}]
})
} else {
return
}
// pass = cmd_buffer.swapchain_pass(_window, {
// color_targets: [{
// load: "clear",
// store: "store",
// clear_color: {r: 0, g: 0, b: 0, a: 1}
// }]
// })
} else {
pass = cmd_buffer.render_pass({
color_targets: [{
texture: output.texture,
load: "clear",
store: "store",
clear_color: {r: 0, g: 0, b: 0, a: 0}
}]
})
}
pass.bind_pipeline(pipeline)
pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}])
pass.bind_index_buffer({buffer: ib, offset: 0}, 16)
// Bind samplers
var samplers = [{texture: input.texture, sampler: _sampler_linear}]
if (cmd.extra_inputs) {
for (var extra of cmd.extra_inputs) {
samplers.push({texture: extra.texture, sampler: _sampler_linear})
}
}
pass.bind_fragment_samplers(0, samplers)
if (uniform_data) {
cmd_buffer.push_fragment_uniform_data(0, uniform_data)
}
pass.draw_indexed(6, 1, 0, 0, 0)
pass.end()
}
function _build_shader_uniforms(shader, uniforms) {
var data = new blob_mod(64) // 16 floats max
switch (shader) {
case 'threshold':
data.wf(uniforms.threshold || 0.8)
data.wf(uniforms.intensity || 1.0)
data.wf(0) // padding
data.wf(0) // padding
break
case 'blur':
var dir = uniforms.direction || {x: 1, y: 0}
var texel = uniforms.texel_size || {x: 0.001, y: 0.001}
data.wf(dir.x)
data.wf(dir.y)
data.wf(texel.x)
data.wf(texel.y)
break
case 'crt':
data.wf(uniforms.curvature || 0.1)
data.wf(uniforms.scanline_intensity || 0.3)
data.wf(uniforms.vignette || 0.2)
data.wf(0) // padding
var res = uniforms.resolution || {width: 1280, height: 720}
data.wf(res.width)
data.wf(res.height)
data.wf(0) // padding
data.wf(0) // padding
break
case 'accumulator':
data.wf(uniforms.decay != null ? uniforms.decay : 0.9)
data.wf(0) // padding
data.wf(0) // padding
data.wf(0) // padding
break
case 'mask':
// channel: 0=alpha, 1=luminance
// invert: 0=normal, 1=inverted
data.wf(uniforms.channel != null ? uniforms.channel : 0)
data.wf(uniforms.invert != null ? uniforms.invert : 0)
data.wf(0) // padding
data.wf(0) // padding
break
default:
return null
}
return stone(data)
}
function _do_composite(cmd_buffer, cmd) {
var base = cmd.base
var overlay = cmd.overlay
var output = cmd.output
var mode = cmd.mode || 'over'
if (!base || !base.texture || !overlay || !overlay.texture || !output || !output.texture) return
// Build fullscreen quad
var geom = _build_fullscreen_quad({x: 0, y: 0, width: output.width, height: output.height}, output.width, output.height)
var vb_size = geom.vertices.length / 8
var ib_size = geom.indices.length / 8
var vb = new gpu_mod.buffer(_gpu, {size: vb_size, vertex: true})
var ib = new gpu_mod.buffer(_gpu, {size: ib_size, index: true})
var vb_transfer = new gpu_mod.transfer_buffer(_gpu, {size: vb_size, usage: "upload"})
var ib_transfer = new gpu_mod.transfer_buffer(_gpu, {size: ib_size, usage: "upload"})
vb_transfer.copy_blob(_gpu, geom.vertices)
ib_transfer.copy_blob(_gpu, geom.indices)
var copy_cmd = _gpu.acquire_cmd_buffer()
var copy = copy_cmd.copy_pass()
copy.upload_to_buffer({transfer_buffer: vb_transfer, offset: 0}, {buffer: vb, offset: 0, size: vb_size}, false)
copy.upload_to_buffer({transfer_buffer: ib_transfer, offset: 0}, {buffer: ib, offset: 0, size: ib_size}, false)
copy.end()
copy_cmd.submit()
// First render base to output (clear and draw)
var base_pass = cmd_buffer.render_pass({
color_targets: [{
texture: output.texture,
load: "clear",
store: "store",
clear_color: {r: 0, g: 0, b: 0, a: 0}
}]
})
base_pass.bind_pipeline(_pipelines.blit)
base_pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}])
base_pass.bind_index_buffer({buffer: ib, offset: 0}, 16)
base_pass.bind_fragment_samplers(0, [{texture: base.texture, sampler: _sampler_nearest}])
base_pass.draw_indexed(6, 1, 0, 0, 0)
base_pass.end()
// Then render overlay with blend mode (load and blend on top)
var overlay_pass = cmd_buffer.render_pass({
color_targets: [{
texture: output.texture,
load: "load",
store: "store"
}]
})
// Use additive blend pipeline for bloom - use blit_add which has correct 16-byte vertex format
var pipeline = mode == 'add' ? _pipelines.blit_add : _pipelines.blit
overlay_pass.bind_pipeline(pipeline)
overlay_pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}])
overlay_pass.bind_index_buffer({buffer: ib, offset: 0}, 16)
overlay_pass.bind_fragment_samplers(0, [{texture: overlay.texture, sampler: _sampler_linear}])
overlay_pass.draw_indexed(6, 1, 0, 0, 0)
overlay_pass.end()
}
return sdl_gpu