This commit is contained in:
2026-01-08 17:55:50 -06:00
parent dec26f6b41
commit ce479299eb
4 changed files with 838 additions and 1 deletions

View File

@@ -36,6 +36,7 @@ var _crt_frag = null
var _accumulator_frag = null
var _text_sdf_frag = null
var _text_msdf_frag = null
var _shape2d_frag = null
// Pipelines
var _pipelines = {}
@@ -259,6 +260,18 @@ function _load_shaders() {
})
}
var shape2d_frag_code = io.slurp("shaders/msl/shape2d.frag.msl")
if (shape2d_frag_code) {
_shape2d_frag = new gpu_mod.shader(_gpu, {
code: shape2d_frag_code,
stage: "fragment",
format: "msl",
entrypoint: "fragment_main",
num_uniform_buffers: 1,
num_samplers: 0
})
}
return true
}
@@ -582,6 +595,7 @@ function _create_pipelines() {
}]
}
})
}
// Accumulator pipeline
if (_blit_vert && _accumulator_frag) {
_pipelines.accumulator = new gpu_mod.graphics_pipeline(_gpu, {
@@ -604,7 +618,43 @@ function _create_pipelines() {
color_targets: [{format: _swapchain_format, blend: {enabled: false}}]
}
})
} }
}
// Shape2D pipeline
if (_sprite_vert && _shape2d_frag) {
_pipelines.shape2d = new gpu_mod.graphics_pipeline(_gpu, {
vertex: _sprite_vert,
fragment: _shape2d_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"
}
}]
}
})
}
}
// ========================================================================
@@ -1155,6 +1205,10 @@ function _execute_commands(commands, window_size) {
pending_draws.push(cmd)
break
case 'draw_shape':
pending_draws.push(cmd)
break
case 'blit':
// Flush pending draws first
if (current_pass && pending_draws.length > 0) {
@@ -1357,6 +1411,13 @@ function _flush_draws(cmd_buffer, pass, draws, camera, target) {
// Render pre-rendered effect texture
_render_texture_ref(cmd_buffer, pass, draw.drawable, camera, target)
} else if (draw.cmd == 'draw_shape') {
// Flush current batch
if (current_batch) _render_batch(cmd_buffer, pass, current_batch, camera, target)
current_batch = null
// Render shape immediately
_render_shape(cmd_buffer, pass, draw.drawable, camera, target)
}
}
@@ -1472,6 +1533,147 @@ function _render_texture_ref(cmd_buffer, pass, drawable, camera, target) {
pass.draw_indexed(geom.index_count, 1, 0, 0, 0)
}
function _render_shape(cmd_buffer, pass, drawable, camera, target) {
if (!_pipelines.shape2d) return
var pos = drawable.pos || {x: 0, y: 0}
var w = drawable.width || 100
var h = drawable.height || 100
var ax = drawable.anchor_x != null ? drawable.anchor_x : 0.5
var ay = drawable.anchor_y != null ? drawable.anchor_y : 0.5
// Calculate padding required for stroke and feather
var stroke_thickness = drawable.stroke_thickness || 0
var feather = drawable.feather != null ? drawable.feather : 0.5
// Resolve stroke alignment (0=inside, 0.5=center, 1=outside)
var stroke_aligns = {inside: 0, center: 0.5, outside: 1}
var sa = drawable.stroke_align in stroke_aligns ? stroke_aligns[drawable.stroke_align] : 0.5
var pad = feather
if (stroke_thickness > 0) {
if (sa > 0.75) pad += stroke_thickness // Outside
else if (sa > 0.25) pad += stroke_thickness * 0.5 // Center
// Inside adds 0
}
// Expand quad by padding
var x = pos.x - w * ax - pad
var y = pos.y - h * ay - pad
var qw = w + pad * 2
var qh = h + pad * 2
// Expand UVs to match padding (p calculation depends on this)
// logical size is w, h. padded size is qw, qh.
// 0..1 maps to w, h.
// We need UVs such that (uv - 0.5) * w spans the padded area logic.
// u=0 -> -0.5 * w = left edge of shape
// target left edge is -pad relative to shape left edge.
// So we need uv such that (uv - 0.5) * w = -w/2 - pad
// uv * w - w/2 = -w/2 - pad
// uv * w = -pad => uv = -pad / w
var u0 = -pad / w
var v0 = -pad / h
var u1 = 1.0 + pad / w
var v1 = 1.0 + pad / h
var fill = drawable.fill || {r: 1, g: 1, b: 1, a: 1}
var opacity = drawable.opacity != null ? drawable.opacity : 1
// Vertex data: pos(2) + uv(2) + color(4) = 8 floats = 32 bytes per vertex
var vertex_data = new blob_mod(4 * 32)
var index_data = new blob_mod(6 * 2)
// v0: bottom-left
vertex_data.wf(x); vertex_data.wf(y)
vertex_data.wf(u0); vertex_data.wf(v1)
vertex_data.wf(1); vertex_data.wf(1); vertex_data.wf(1); vertex_data.wf(1)
// v1: bottom-right
vertex_data.wf(x + qw); vertex_data.wf(y)
vertex_data.wf(u1); vertex_data.wf(v1)
vertex_data.wf(1); vertex_data.wf(1); vertex_data.wf(1); vertex_data.wf(1)
// v2: top-right
vertex_data.wf(x + qw); vertex_data.wf(y + qh)
vertex_data.wf(u1); vertex_data.wf(v0)
vertex_data.wf(1); vertex_data.wf(1); vertex_data.wf(1); vertex_data.wf(1)
// v3: top-left
vertex_data.wf(x); vertex_data.wf(y + qh)
vertex_data.wf(u0); vertex_data.wf(v0)
vertex_data.wf(1); vertex_data.wf(1); vertex_data.wf(1); vertex_data.wf(1)
// Indices
index_data.w16(0); index_data.w16(1); index_data.w16(2)
index_data.w16(0); index_data.w16(2); index_data.w16(3)
// Upload geometry
var vb_size = 4 * 32
var ib_size = 6 * 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, stone(vertex_data))
ib_transfer.copy_blob(_gpu, stone(index_data))
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)
// Build shape uniforms - must match ShapeParams struct in shader
// Total size: 112 bytes
var stroke = drawable.stroke || {r: 0, g: 0, b: 0, a: 0}
var shape_types = {rect: 0, circle: 1, ellipse: 2, pill: 3}
var shape_type = shape_types[drawable.shape_type] || 0
var u_data = new blob_mod(112)
u_data.wf(fill.r); u_data.wf(fill.g); u_data.wf(fill.b); u_data.wf(fill.a) // fill_color (16)
u_data.wf(stroke.r); u_data.wf(stroke.g); u_data.wf(stroke.b); u_data.wf(stroke.a) // stroke_color (16)
u_data.wf(w); u_data.wf(h) // size (8) - PASS LOGICAL SIZE
u_data.wf(drawable.radius || 0) // radius (4)
u_data.wf(feather) // feather (4)
u_data.wf(stroke_thickness) // stroke_thickness (4)
u_data.wf(sa) // stroke_align (4)
u_data.w32(shape_type) // shape_type (4)
u_data.w32(0) // corner_style (4)
u_data.wf(drawable.dash_len || 0) // dash_len (4)
u_data.wf(drawable.gap_len || 0) // gap_len (4)
u_data.wf(drawable.dash_offset || 0) // dash_offset (4)
u_data.w32(0) // cap_type (4)
// uv_transform (16)
if (drawable.uv && drawable.uv.scale) {
u_data.wf(drawable.uv.scale.x); u_data.wf(drawable.uv.scale.y)
u_data.wf(drawable.uv.offset.x); u_data.wf(drawable.uv.offset.y)
} else {
u_data.wf(1); u_data.wf(1); u_data.wf(0); u_data.wf(0)
}
u_data.wf(drawable.uv && drawable.uv.rotate ? drawable.uv.rotate : 0) // uv_rotate (4)
u_data.w32(0) // has_texture (4)
u_data.wf(opacity) // opacity (4)
u_data.wf(0) // _pad (4)
pass.bind_pipeline(_pipelines.shape2d)
pass.bind_vertex_buffers(0, [{buffer: vb, offset: 0}])
pass.bind_index_buffer({buffer: ib, offset: 0}, 16)
pass.bind_fragment_samplers(0, [{texture: _white_texture, sampler: _sampler_linear}])
cmd_buffer.push_vertex_uniform_data(0, proj)
cmd_buffer.push_fragment_uniform_data(0, stone(u_data))
pass.draw_indexed(6, 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