2044 lines
59 KiB
Plaintext
2044 lines
59 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 tint and opacity
|
|
var tint = s.tint || white
|
|
var opacity = s.opacity != null ? s.opacity : 1
|
|
var final_r = c.r * tint.r
|
|
var final_g = c.g * tint.g
|
|
var final_b = c.b * tint.b
|
|
var final_a = c.a * (tint.a != null ? tint.a : 1) * opacity
|
|
|
|
// 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
|
|
|
|
// Apply UV transform (offset, scale, rotate)
|
|
var uv = s.uv
|
|
if (uv) {
|
|
var uv_off = uv.offset || {x: 0, y: 0}
|
|
var uv_scale = uv.scale || {x: 1, y: 1}
|
|
// Apply scale and offset to UVs
|
|
u0 = u0 * uv_scale.x + uv_off.x
|
|
v0 = v0 * uv_scale.y + uv_off.y
|
|
u1 = u1 * uv_scale.x + uv_off.x
|
|
v1 = v1 * uv_scale.y + uv_off.y
|
|
}
|
|
|
|
// Apply flip
|
|
var flip = s.flip
|
|
if (flip) {
|
|
if (flip.x) {
|
|
var tmp = u0
|
|
u0 = u1
|
|
u1 = tmp
|
|
}
|
|
if (flip.y) {
|
|
var tmp = v0
|
|
v0 = v1
|
|
v1 = tmp
|
|
}
|
|
}
|
|
|
|
// 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(final_r)
|
|
vertex_data.wf(final_g)
|
|
vertex_data.wf(final_b)
|
|
vertex_data.wf(final_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(final_r)
|
|
vertex_data.wf(final_g)
|
|
vertex_data.wf(final_b)
|
|
vertex_data.wf(final_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(final_r)
|
|
vertex_data.wf(final_g)
|
|
vertex_data.wf(final_b)
|
|
vertex_data.wf(final_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(final_r)
|
|
vertex_data.wf(final_g)
|
|
vertex_data.wf(final_b)
|
|
vertex_data.wf(final_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'
|
|
var sampler = draw.material ? draw.material.sampler : 'nearest'
|
|
|
|
// 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 &&
|
|
current_batch.sampler == sampler) {
|
|
|
|
// 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,
|
|
sampler: sampler,
|
|
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
|
|
|
|
// Select sampler based on filter
|
|
var sampler = (batch.sampler == 'linear') ? _sampler_linear : _sampler_nearest
|
|
|
|
// 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}])
|
|
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: "load", // Load existing content to blend layers properly
|
|
store: "store"
|
|
}]
|
|
})
|
|
|
|
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
|