From b8328657df938e9e9a7a535dff7bbfa46581eec5 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Mon, 14 Apr 2025 22:46:07 -0500 Subject: [PATCH] add camera test --- scripts/modules/draw2d.js | 2 +- scripts/modules/render.js | 791 +--------------------------------- scripts/modules/sdl_gpu.js | 791 ++++++++++++++++++++++++++++++++++ scripts/modules/sdl_render.js | 31 +- source/jsffi.c | 2 +- tests/camera.js | 15 + 6 files changed, 838 insertions(+), 794 deletions(-) create mode 100644 scripts/modules/sdl_gpu.js create mode 100644 tests/camera.js diff --git a/scripts/modules/draw2d.js b/scripts/modules/draw2d.js index 31c8e999..d0038dec 100644 --- a/scripts/modules/draw2d.js +++ b/scripts/modules/draw2d.js @@ -3,7 +3,7 @@ var graphics = use('graphics') var math = use('math') var util = use('util') var os = use('os') -var geometry = use('geomtry') +var geometry = use('geometry') var draw = {} draw[prosperon.DOC] = ` diff --git a/scripts/modules/render.js b/scripts/modules/render.js index ac2e2ba8..a2069c9f 100644 --- a/scripts/modules/render.js +++ b/scripts/modules/render.js @@ -1,790 +1 @@ -var render = {} - -var io = use('io') -var os = use('os') -var controller = use('controller') -var tracy = use('tracy') -var graphics = use('graphics') -var imgui = use('imgui') - -var base_pipeline = { - vertex: "sprite.vert", - fragment: "sprite.frag", - primitive: "triangle", // point, line, linestrip, triangle, trianglestrip - fill: true, // false for lines - depth: { - compare: "greater_equal", // never/less/equal/less_equal/greater/not_equal/greater_equal/always - test: false, - write: false, - bias: 0, - bias_slope_scale: 0, - bias_clamp: 0 - }, - stencil: { - enabled: true, - front: { - compare: "equal", // never/less/equal/less_equal/greater/neq/greq/always - fail: "keep", // keep/zero/replace/incr_clamp/decr_clamp/invert/incr_wrap/decr_wrap - depth_fail: "keep", - pass: "keep" - }, - back: { - compare: "equal", // never/less/equal/less_equal/greater/neq/greq/always - fail: "keep", // keep/zero/replace/incr_clamp/decr_clamp/invert/incr_wrap/decr_wrap - depth_fail: "keep", - pass: "keep" - }, - test: true, - compare_mask: 0, - write_mask: 0 - }, - blend: { - enabled: false, - src_rgb: "zero", // zero/one/src_color/one_minus_src_color/dst_color/one_minus_dst_color/src_alpha/one_minus_src_alpha/dst_alpha/one_minus_dst_alpha/constant_color/one_minus_constant_color/src_alpha_saturate - dst_rgb: "zero", - op_rgb: "add", // add/sub/rev_sub/min/max - src_alpha: "one", - dst_alpha: "zero", - op_alpha: "add" - }, - cull: "none", // none/front/back - face: "cw", // cw/ccw - alpha_to_coverage: false, - multisample: { - count: 1, // number of multisamples - mask: 0xFFFFFFFF, - domask: false - }, - label: "scripted pipeline", - target: {} -} - -var sprite_pipeline = Object.create(base_pipeline); -sprite_pipeline.blend = { - enabled:true, - src_rgb: "src_alpha", // zero/one/src_color/one_minus_src_color/dst_color/one_minus_dst_color/src_alpha/one_minus_src_alpha/dst_alpha/one_minus_dst_alpha/constant_color/one_minus_constant_color/src_alpha_saturate - dst_rgb: "one_minus_src_alpha", - op_rgb: "add", // add/sub/rev_sub/min/max - src_alpha: "one", - dst_alpha: "zero", - op_alpha: "add" -}; - -var context; - -sprite_pipeline.target = { - color_targets: [{ - format:"rgba8", - blend:sprite_pipeline.blend - }], - depth: "d32 float s8" -}; - -var driver = "vulkan" -switch(os.platform()) { - case "Linux": - driver = "vulkan" - break - case "Windows": -// driver = "direct3d12" - driver = "vulkan" - break - case "macOS": - driver = "metal" - break -} - -var unit_transform = os.make_transform(); - -var cur = {}; -cur.images = []; -cur.samplers = []; - -var tbuffer; -function full_upload(buffers) { - var cmds = context.acquire_cmd_buffer(); - tbuffer = context.upload(cmds, buffers, tbuffer); - cmds.submit(); -} - -full_upload[prosperon.DOC] = `Acquire a command buffer and upload the provided data buffers to the GPU, then submit. - -:param buffers: An array of data buffers to be uploaded. -:return: None -` - -function bind_pipeline(pass, pipeline) { - make_pipeline(pipeline) - pass.bind_pipeline(pipeline.gpu) - pass.pipeline = pipeline; -} - -bind_pipeline[prosperon.DOC] = `Ensure the specified pipeline is created on the GPU and bind it to the given render pass. - -:param pass: The current render pass to bind the pipeline to. -:param pipeline: The pipeline object containing shader and state info. -:return: None -` - -var main_pass; - -var cornflower = [62/255,96/255,113/255,1]; - -function get_pipeline_ubo_slot(pipeline, name) { - if (!pipeline.vertex.reflection.ubos) return; - for (var i = 0; i < pipeline.vertex.reflection.ubos.length; i++) { - var ubo = pipeline.vertex.reflection.ubos[i]; - if (ubo.name.endsWith(name)) - return i; - } - return undefined; -} - -get_pipeline_ubo_slot[prosperon.DOC] = `Return the index of a uniform buffer block within the pipeline's vertex reflection data by name suffix. - -:param pipeline: The pipeline whose vertex reflection is inspected. -:param name: A string suffix to match against the uniform buffer block name. -:return: The integer index of the matching UBO, or undefined if not found. -` - -function transpose4x4(val) { - var out = []; - out[0] = val[0]; out[1] = val[4]; out[2] = val[8]; out[3] = val[12]; - out[4] = val[1]; out[5] = val[5]; out[6] = val[9]; out[7] = val[13]; - out[8] = val[2]; out[9] = val[6]; out[10] = val[10];out[11] = val[14]; - out[12] = val[3];out[13] = val[7];out[14] = val[11];out[15] = val[15]; - return out; -} - -transpose4x4[prosperon.DOC] = `Return a new 4x4 matrix array that is the transpose of the passed matrix. - -:param val: An array of length 16 representing a 4x4 matrix in row-major format. -:return: A new array of length 16 representing the transposed matrix. -` - -function ubo_obj_to_array(pipeline, name, obj) { - var ubo; - for (var i = 0; i < pipeline.vertex.reflection.ubos.length; i++) { - ubo = pipeline.vertex.reflection.ubos[i]; - if (ubo.name.endsWith(name)) break; - } - var type = pipeline.vertex.reflection.types[ubo.type]; - var len = 0; - for (var mem of type.members) - len += type_to_byte_count(mem.type); - - var buf = new ArrayBuffer(len); - var view = new DataView(buf); - - for (var mem of type.members) { - var val = obj[mem.name]; - if (!val) throw new Error (`Could not find ${mem.name} on supplied object`); - - if (mem.name === 'model') - val = transpose4x4(val.array()); - - for (var i = 0; i < val.length; i++) - view.setFloat32(mem.offset + i*4, val[i],true); - } - return buf; -} - -ubo_obj_to_array[prosperon.DOC] = `Construct an ArrayBuffer containing UBO data from the provided object, matching the pipeline's reflection info. - -:param pipeline: The pipeline whose vertex reflection is read for UBO structure. -:param name: The name suffix that identifies the target UBO in the reflection data. -:param obj: An object whose properties match the UBO members. -:return: An ArrayBuffer containing packed UBO data. -` - -function type_to_byte_count(type) { - switch (type) { - case 'float': return 4; - case 'vec2': return 8; - case 'vec3': return 12; - case 'vec4': return 16; - case 'mat4': return 64; - default: throw new Error("Unknown or unsupported float-based type: " + type); - } -} - -type_to_byte_count[prosperon.DOC] = `Return the byte size for known float-based types. - -:param type: A string type identifier (e.g., 'float', 'vec2', 'vec3', 'vec4', 'mat4'). -:return: Integer number of bytes. -` - -var sprite_model_ubo = { - model: unit_transform, - color: [1,1,1,1] -}; - -var shader_cache = {}; -var shader_times = {}; - -function make_pipeline(pipeline) { - if (pipeline.hasOwnProperty("gpu")) return; // this pipeline has already been made - - if (typeof pipeline.vertex === 'string') - pipeline.vertex = make_shader(pipeline.vertex); - if (typeof pipeline.fragment === 'string') - pipeline.fragment = make_shader(pipeline.fragment) - - // 1) Reflection data for vertex shader - var refl = pipeline.vertex.reflection - if (!refl || !refl.inputs || !Array.isArray(refl.inputs)) { - pipeline.gpu = context.make_pipeline(pipeline); - return; - } - - var inputs = refl.inputs - var buffer_descriptions = [] - var attributes = [] - - // 2) Build buffer + attribute for each reflection input - for (var i = 0; i < inputs.length; i++) { - var inp = inputs[i] - var typeStr = inp.type - var nameStr = (inp.name || "").toUpperCase() - var pitch = 4 - var fmt = "float1" - - if (typeStr == "vec2") { - pitch = 8 - fmt = "float2" - } else if (typeStr == "vec3") { - pitch = 12 - fmt = "float3" - } else if (typeStr == "vec4") { - if (nameStr.indexOf("COLOR") >= 0) { - pitch = 16 - fmt = "color" - } else { - pitch = 16 - fmt = "float4" - } - } - - buffer_descriptions.push({ - slot: i, - pitch: pitch, - input_rate: "vertex", - instance_step_rate: 0, - name:inp.name.split(".").pop() - }) - - attributes.push({ - location: inp.location, - buffer_slot: i, - format: fmt, - offset: 0 - }) - } - - pipeline.vertex_buffer_descriptions = buffer_descriptions - pipeline.vertex_attributes = attributes - - pipeline.gpu = context.make_pipeline(pipeline); -} - -make_pipeline[prosperon.DOC] = `Create and store a GPU pipeline object if it has not already been created. - -:param pipeline: An object describing the pipeline state, shaders, and reflection data. -:return: None -` - -var shader_type; - -function make_shader(sh_file) { - var file = `shaders/${shader_type}/${sh_file}.${shader_type}` - if (shader_cache[file]) return shader_cache[file] - var refl = json.decode(io.slurp(`shaders/reflection/${sh_file}.json`)) - - var shader = { - code: io.slurpbytes(file), - format: shader_type, - stage: sh_file.endsWith("vert") ? "vertex" : "fragment", - num_samplers: refl.separate_samplers ? refl.separate_samplers.length : 0, - num_textures: 0, - num_storage_buffers: refl.separate_storage_buffers ? refl.separate_storage_buffers.length : 0, - num_uniform_buffers: refl.ubos ? refl.ubos.length : 0, - entrypoint: shader_type === "msl" ? "main0" : "main" - } - - shader.gpu = context.make_shader(shader) - shader.reflection = refl; - shader_cache[file] = shader - shader.file = sh_file - return shader -} - -make_shader[prosperon.DOC] = `Load and compile a shader from disk, caching the result. Reflective metadata is also loaded. - -:param sh_file: The base filename (without extension) of the shader to compile. -:return: A shader object with GPU and reflection data attached. -` - -var render_queue = []; -var hud_queue = []; - -var current_queue = render_queue; - -var std_sampler = { - min_filter: "nearest", - mag_filter: "nearest", - mipmap: "linear", - u: "repeat", - v: "repeat", - w: "repeat", - mip_bias: 0, - max_anisotropy: 0, - compare_op: "none", - min_lod: 0, - max_lod: 0, - anisotropy: false, - compare: false -}; - -function upload_model(model) { - var bufs = []; - for (var i in model) { - if (typeof model[i] !== 'object') continue; - bufs.push(model[i]); - } - context.upload(this, bufs); -} - -upload_model[prosperon.DOC] = `Upload all buffer-like properties of the given model to the GPU. - -:param model: An object whose buffer properties are to be uploaded. -:return: None -` - -function bind_model(pass, pipeline, model) { - var buffers = pipeline.vertex_buffer_descriptions; - var bufs = []; - if (buffers) - for (var b of buffers) { - if (b.name in model) bufs.push(model[b.name]) - else throw Error (`could not find buffer ${b.name} on model`); - } - pass.bind_buffers(0,bufs); - pass.bind_index_buffer(model.indices); -} - -bind_model[prosperon.DOC] = `Bind the model's vertex and index buffers for the given pipeline and render pass. - -:param pass: The current render pass. -:param pipeline: The pipeline object with vertex buffer descriptions. -:param model: The model object containing matching buffers and an index buffer. -:return: None -` - -function bind_mat(pass, pipeline, mat) { - var imgs = []; - var refl = pipeline.fragment.reflection; - if (refl.separate_images) { - for (var i of refl.separate_images) { - if (i.name in mat) { - var tex = mat[i.name]; - imgs.push({texture:tex.texture, sampler:tex.sampler}); - } else - throw Error (`could not find all necessary images: ${i.name}`) - } - pass.bind_samplers(false, 0,imgs); - } -} - -bind_mat[prosperon.DOC] = `Bind the material images and samplers needed by the pipeline's fragment shader. - -:param pass: The current render pass. -:param pipeline: The pipeline whose fragment shader reflection indicates required textures. -:param mat: An object mapping the required image names to {texture, sampler}. -:return: None -` - -function group_sprites_by_texture(sprites, mesh) { - if (sprites.length === 0) return; - for (var i = 0; i < sprites.length; i++) { - sprites[i].mesh = mesh; - sprites[i].first_index = i*6; - sprites[i].num_indices = 6; - } - return; - // The code below is an alternate approach to grouping by image. Currently not in use. - /* - var groups = []; - var group = {image:sprites[0].image, first_index:0}; - var count = 1; - for (var i = 1; i < sprites.length; i++) { - if (sprites[i].image === group.image) { - count++; - continue; - } - group.num_indices = count*6; - var newgroup = {image:sprites[i].image, first_index:group.first_index+group.num_indices}; - group = newgroup; - groups.push(group); - count=1; - } - group.num_indices = count*6; - return groups; - */ -} - -group_sprites_by_texture[prosperon.DOC] = `Assign each sprite to the provided mesh, generating index data as needed. - -:param sprites: An array of sprite objects. -:param mesh: A mesh object (pos, color, uv, indices, etc.) to link to each sprite. -:return: None -` - -var main_color = { - type:"2d", - format: "rgba8", - layers: 1, - mip_levels: 1, - samples: 0, - sampler:true, - color_target:true -}; - -var main_depth = { - type: "2d", - format: "d32 float s8", - layers:1, - mip_levels:1, - samples:0, - sampler:true, - depth_target:true -}; - -function render_camera(cmds, camera) { - var pass; - delete camera.target // TODO: HORRIBLE - if (!camera.target) { - main_color.width = main_depth.width = camera.size.x; - main_color.height = main_depth.height = camera.size.y; - camera.target = { - color_targets: [{ - texture: context.texture(main_color), - mip_level:0, - layer: 0, - load:"clear", - store:"store", - clear: cornflower - }], - depth_stencil: { - texture: context.texture(main_depth), - clear:1, - load:"dont_care", - store:"dont_care", - stencil_load:"dont_care", - stencil_store:"dont_care", - stencil_clear:0 - } - }; - } - - var buffers = []; - buffers = buffers.concat(graphics.queue_sprite_mesh(render_queue)); - var unique_meshes = [...new Set(render_queue.map(x => x.mesh))]; - for (var q of unique_meshes) - buffers = buffers.concat([q.pos, q.color, q.uv, q.indices]); - - buffers = buffers.concat(graphics.queue_sprite_mesh(hud_queue)); - for (var q of hud_queue) - if (q.type === 'geometry') buffers = buffers.concat([q.mesh.pos, q.mesh.color, q.mesh.uv, q.mesh.indices]); - - full_upload(buffers) - - var pass = cmds.render_pass(camera.target); - - var pipeline = sprite_pipeline; - bind_pipeline(pass,pipeline); - - var camslot = get_pipeline_ubo_slot(pipeline, 'TransformBuffer'); - if (typeof camslot !== 'undefined') - cmds.camera(camera, camslot); - - modelslot = get_pipeline_ubo_slot(pipeline, "model"); - if (typeof modelslot !== 'undefined') { - var ubo = ubo_obj_to_array(pipeline, 'model', sprite_model_ubo); - cmds.push_vertex_uniform_data(modelslot, ubo); - } - - var mesh; - var img; - var modelslot; - - cmds.push_debug_group("draw") - for (var group of render_queue) { - if (mesh != group.mesh) { - mesh = group.mesh; - bind_model(pass,pipeline,mesh); - } - - if (group.image && img != group.image) { - img = group.image; - img.sampler = std_sampler; - bind_mat(pass,pipeline,{diffuse:img}); - } - - pass.draw_indexed(group.num_indices, 1, group.first_index, 0, 0); - } - cmds.pop_debug_group() - - cmds.push_debug_group("hud") - var camslot = get_pipeline_ubo_slot(pipeline, 'TransformBuffer'); - if (typeof camslot !== 'undefined') - cmds.hud(camera.size, camslot); - - for (var group of hud_queue) { - if (mesh != group.mesh) { - mesh = group.mesh; - bind_model(pass,pipeline,mesh); - } - - if (group.image && img != group.image) { - img = group.image; - img.sampler = std_sampler; - bind_mat(pass,pipeline,{diffuse:img}); - } - - pass.draw_indexed(group.num_indices, 1, group.first_index, 0, 0); - } - cmds.pop_debug_group(); - - pass?.end(); - - render_queue = []; - hud_queue = []; -} - -render_camera[prosperon.DOC] = `Render a scene using the provided camera, drawing both render queue and HUD queue items. - -:param cmds: A command buffer obtained from the GPU context. -:param camera: The camera object (with size, optional target, etc.). -:return: None -` - -var swaps = []; -render.present = function() { - os.clean_transforms(); - var cmds = context.acquire_cmd_buffer(); - render_camera(cmds, prosperon.camera); - var swapchain_tex = cmds.acquire_swapchain(); - if (!swapchain_tex) - cmds.cancel(); - else { - var torect = prosperon.camera.draw_rect(prosperon.window.size); - torect.texture = swapchain_tex; - if (swapchain_tex) { - cmds.blit({ - src: prosperon.camera.target.color_targets[0].texture, - dst: torect, - filter:"nearest", - load: "clear" - }); - - if (imgui) { // draws any imgui commands present - cmds.push_debug_group("imgui") - imgui.prepend(cmds); - var pass = cmds.render_pass({ - color_targets:[{texture:swapchain_tex}]}); - imgui.endframe(cmds,pass); - pass.end(); - cmds.pop_debug_group() - } - } - cmds.submit() - } -} - -render.present[prosperon.DOC] = `Perform the per-frame rendering and present the final swapchain image, including imgui pass if available. - -:return: None -` - -var stencil_write = { - compare: "always", - fail_op: "replace", - depth_fail_op: "replace", - pass_op: "replace" -}; - -function stencil_writer(ref) { - var pipe = Object.create(base_pipeline); - Object.assign(pipe, { - stencil: { - enabled: true, - front: stencil_write, - back: stencil_write, - write:true, - read:true, - ref:ref - }, - write_mask: colormask.none - }); - return pipe; -}.hashify(); - -// objects by default draw where the stencil buffer is 0 -function fillmask(ref) { - var pipe = stencil_writer(ref); - render.use_shader('screenfill.cg', pipe); - render.draw(shape.quad); -} - -render.fillmask[prosperon.DOC] = `Draw a fullscreen shape using a 'screenfill' shader to populate the stencil buffer with a given reference. - -:param ref: The stencil reference value to write. -:return: None -` - -var stencil_invert = { - compare: "always", - fail_op: "invert", - depth_fail_op: "invert", - pass_op: "invert" -}; - -function mask(image, pos, scale, rotation = 0, ref = 1) { - if (typeof image === 'string') - image = graphics.texture(image); - - var tex = image.texture; - if (scale) scale = scale.div([tex.width,tex.height]); - else scale = [1,1,1] - - var pipe = stencil_writer(ref); - render.use_shader('sprite.cg', pipe); - var t = os.make_transform(); - t.trs(pos, undefined, scale); - set_model(t); - render.use_mat({ - diffuse:image.texture, - rect: image.rect, - shade: Color.white - }); - render.draw(shape.quad); -} - -render.mask[prosperon.DOC] = `Draw an image to the stencil buffer, marking its area with a specified reference value. - -:param image: A texture or string path (which is converted to a texture). -:param pos: The translation (x, y) for the image placement. -:param scale: Optional scaling applied to the texture. -:param rotation: Optional rotation in radians (unused by default). -:param ref: The stencil reference value to write. -:return: None -` - -render.viewport = function(rect) { - context.viewport(rect); -} - -render.viewport[prosperon.DOC] = `Set the GPU viewport to the specified rectangle. - -:param rect: A rectangle [x, y, width, height]. -:return: None -` - -render.scissor = function(rect) { - render.viewport(rect) -} - -render.scissor[prosperon.DOC] = `Set the GPU scissor region to the specified rectangle (alias of render.viewport). - -:param rect: A rectangle [x, y, width, height]. -:return: None -` - -var std_sampler - -if (tracy) tracy.gpu_init() - -render.queue = function(cmd) { - if (Array.isArray(cmd)) - for (var i of cmd) current_queue.push(i) - else - current_queue.push(cmd) -} - -render.queue[prosperon.DOC] = `Enqueue one or more draw commands. These commands are batched until render_camera is called. - -:param cmd: Either a single command object or an array of command objects. -:return: None -` - -render.setup_draw = function() { - current_queue = render_queue; - prosperon.draw(); -} - -render.setup_draw[prosperon.DOC] = `Switch the current queue to the primary scene render queue, then invoke 'prosperon.draw' if defined. - -:return: None -` - -render.setup_hud = function() { - current_queue = hud_queue; - prosperon.hud(); -} - -render.setup_hud[prosperon.DOC] = `Switch the current queue to the HUD render queue, then invoke 'prosperon.hud' if defined. - -:return: None -` - -render.initialize = function(config) -{ - var default_conf = { - title:`Prosperon [${prosperon.version}-${prosperon.revision}]`, - width: 1280, - height: 720, - icon: graphics.make_texture(io.slurpbytes('icons/moon.gif')), - high_dpi:0, - alpha:1, - fullscreen:0, - sample_count:1, - enable_clipboard:true, - enable_dragndrop: true, - max_dropped_files: 1, - swap_interval: 1, - name: "Prosperon", - version:prosperon.version + "-" + prosperon.revision, - identifier: "world.pockle.prosperon", - creator: "Pockle World LLC", - copyright: "Copyright Pockle World 2025", - type: "game", - url: "https://prosperon.dev" - } - - config.__proto__ = default_conf - - prosperon.camera = use('ext/camera').make() - prosperon.camera.size = [config.width,config.height] - - prosperon.window = prosperon.engine_start(config) - - context = prosperon.window.make_gpu(false,driver) - context.window = prosperon.window - context.claim_window(prosperon.window) - context.set_swapchain('sdr', 'vsync') - - if (imgui) imgui.init(context, prosperon.window) - - shader_type = context.shader_format()[0]; - - std_sampler = context.make_sampler({ - min_filter: "nearest", - mag_filter: "nearest", - mipmap_mode: "nearest", - address_mode_u: "repeat", - address_mode_v: "repeat", - address_mode_w: "repeat" - }); -} - -return render +return use('sdl_render') diff --git a/scripts/modules/sdl_gpu.js b/scripts/modules/sdl_gpu.js new file mode 100644 index 00000000..f1c2e97f --- /dev/null +++ b/scripts/modules/sdl_gpu.js @@ -0,0 +1,791 @@ +var render = {} + +var io = use('io') +var os = use('os') +var controller = use('controller') +var tracy = use('tracy') +var graphics = use('graphics') +var imgui = use('imgui') + +var base_pipeline = { + vertex: "sprite.vert", + fragment: "sprite.frag", + primitive: "triangle", // point, line, linestrip, triangle, trianglestrip + fill: true, // false for lines + depth: { + compare: "greater_equal", // never/less/equal/less_equal/greater/not_equal/greater_equal/always + test: false, + write: false, + bias: 0, + bias_slope_scale: 0, + bias_clamp: 0 + }, + stencil: { + enabled: true, + front: { + compare: "equal", // never/less/equal/less_equal/greater/neq/greq/always + fail: "keep", // keep/zero/replace/incr_clamp/decr_clamp/invert/incr_wrap/decr_wrap + depth_fail: "keep", + pass: "keep" + }, + back: { + compare: "equal", // never/less/equal/less_equal/greater/neq/greq/always + fail: "keep", // keep/zero/replace/incr_clamp/decr_clamp/invert/incr_wrap/decr_wrap + depth_fail: "keep", + pass: "keep" + }, + test: true, + compare_mask: 0, + write_mask: 0 + }, + blend: { + enabled: false, + src_rgb: "zero", // zero/one/src_color/one_minus_src_color/dst_color/one_minus_dst_color/src_alpha/one_minus_src_alpha/dst_alpha/one_minus_dst_alpha/constant_color/one_minus_constant_color/src_alpha_saturate + dst_rgb: "zero", + op_rgb: "add", // add/sub/rev_sub/min/max + src_alpha: "one", + dst_alpha: "zero", + op_alpha: "add" + }, + cull: "none", // none/front/back + face: "cw", // cw/ccw + alpha_to_coverage: false, + multisample: { + count: 1, // number of multisamples + mask: 0xFFFFFFFF, + domask: false + }, + label: "scripted pipeline", + target: {} +} + +var sprite_pipeline = Object.create(base_pipeline); +sprite_pipeline.blend = { + enabled:true, + src_rgb: "src_alpha", // zero/one/src_color/one_minus_src_color/dst_color/one_minus_dst_color/src_alpha/one_minus_src_alpha/dst_alpha/one_minus_dst_alpha/constant_color/one_minus_constant_color/src_alpha_saturate + dst_rgb: "one_minus_src_alpha", + op_rgb: "add", // add/sub/rev_sub/min/max + src_alpha: "one", + dst_alpha: "zero", + op_alpha: "add" +}; + +var context; + +sprite_pipeline.target = { + color_targets: [{ + format:"rgba8", + blend:sprite_pipeline.blend + }], + depth: "d32 float s8" +}; + +var driver = "vulkan" +switch(os.platform()) { + case "Linux": + driver = "vulkan" + break + case "Windows": +// driver = "direct3d12" + driver = "vulkan" + break + case "macOS": + driver = "metal" + break +} + +var unit_transform = os.make_transform(); + +var cur = {}; +cur.images = []; +cur.samplers = []; + +var tbuffer; +function full_upload(buffers) { + var cmds = context.acquire_cmd_buffer(); + tbuffer = context.upload(cmds, buffers, tbuffer); + cmds.submit(); +} + +full_upload[prosperon.DOC] = `Acquire a command buffer and upload the provided data buffers to the GPU, then submit. + +:param buffers: An array of data buffers to be uploaded. +:return: None +` + +function bind_pipeline(pass, pipeline) { + make_pipeline(pipeline) + pass.bind_pipeline(pipeline.gpu) + pass.pipeline = pipeline; +} + +bind_pipeline[prosperon.DOC] = `Ensure the specified pipeline is created on the GPU and bind it to the given render pass. + +:param pass: The current render pass to bind the pipeline to. +:param pipeline: The pipeline object containing shader and state info. +:return: None +` + +var main_pass; + +var cornflower = [62/255,96/255,113/255,1]; + +function get_pipeline_ubo_slot(pipeline, name) { + if (!pipeline.vertex.reflection.ubos) return; + for (var i = 0; i < pipeline.vertex.reflection.ubos.length; i++) { + var ubo = pipeline.vertex.reflection.ubos[i]; + if (ubo.name.endsWith(name)) + return i; + } + return undefined; +} + +get_pipeline_ubo_slot[prosperon.DOC] = `Return the index of a uniform buffer block within the pipeline's vertex reflection data by name suffix. + +:param pipeline: The pipeline whose vertex reflection is inspected. +:param name: A string suffix to match against the uniform buffer block name. +:return: The integer index of the matching UBO, or undefined if not found. +` + +function transpose4x4(val) { + var out = []; + out[0] = val[0]; out[1] = val[4]; out[2] = val[8]; out[3] = val[12]; + out[4] = val[1]; out[5] = val[5]; out[6] = val[9]; out[7] = val[13]; + out[8] = val[2]; out[9] = val[6]; out[10] = val[10];out[11] = val[14]; + out[12] = val[3];out[13] = val[7];out[14] = val[11];out[15] = val[15]; + return out; +} + +transpose4x4[prosperon.DOC] = `Return a new 4x4 matrix array that is the transpose of the passed matrix. + +:param val: An array of length 16 representing a 4x4 matrix in row-major format. +:return: A new array of length 16 representing the transposed matrix. +` + +function ubo_obj_to_array(pipeline, name, obj) { + var ubo; + for (var i = 0; i < pipeline.vertex.reflection.ubos.length; i++) { + ubo = pipeline.vertex.reflection.ubos[i]; + if (ubo.name.endsWith(name)) break; + } + var type = pipeline.vertex.reflection.types[ubo.type]; + var len = 0; + for (var mem of type.members) + len += type_to_byte_count(mem.type); + + var buf = new ArrayBuffer(len); + var view = new DataView(buf); + + for (var mem of type.members) { + var val = obj[mem.name]; + if (!val) throw new Error (`Could not find ${mem.name} on supplied object`); + + if (mem.name === 'model') + val = transpose4x4(val.array()); + + for (var i = 0; i < val.length; i++) + view.setFloat32(mem.offset + i*4, val[i],true); + } + return buf; +} + +ubo_obj_to_array[prosperon.DOC] = `Construct an ArrayBuffer containing UBO data from the provided object, matching the pipeline's reflection info. + +:param pipeline: The pipeline whose vertex reflection is read for UBO structure. +:param name: The name suffix that identifies the target UBO in the reflection data. +:param obj: An object whose properties match the UBO members. +:return: An ArrayBuffer containing packed UBO data. +` + +function type_to_byte_count(type) { + switch (type) { + case 'float': return 4; + case 'vec2': return 8; + case 'vec3': return 12; + case 'vec4': return 16; + case 'mat4': return 64; + default: throw new Error("Unknown or unsupported float-based type: " + type); + } +} + +type_to_byte_count[prosperon.DOC] = `Return the byte size for known float-based types. + +:param type: A string type identifier (e.g., 'float', 'vec2', 'vec3', 'vec4', 'mat4'). +:return: Integer number of bytes. +` + +var sprite_model_ubo = { + model: unit_transform, + color: [1,1,1,1] +}; + +var shader_cache = {}; +var shader_times = {}; + +function make_pipeline(pipeline) { + if (pipeline.hasOwnProperty("gpu")) return; // this pipeline has already been made + + if (typeof pipeline.vertex === 'string') + pipeline.vertex = make_shader(pipeline.vertex); + if (typeof pipeline.fragment === 'string') + pipeline.fragment = make_shader(pipeline.fragment) + + // 1) Reflection data for vertex shader + var refl = pipeline.vertex.reflection + if (!refl || !refl.inputs || !Array.isArray(refl.inputs)) { + pipeline.gpu = context.make_pipeline(pipeline); + return; + } + + var inputs = refl.inputs + var buffer_descriptions = [] + var attributes = [] + + // 2) Build buffer + attribute for each reflection input + for (var i = 0; i < inputs.length; i++) { + var inp = inputs[i] + var typeStr = inp.type + var nameStr = (inp.name || "").toUpperCase() + var pitch = 4 + var fmt = "float1" + + if (typeStr == "vec2") { + pitch = 8 + fmt = "float2" + } else if (typeStr == "vec3") { + pitch = 12 + fmt = "float3" + } else if (typeStr == "vec4") { + if (nameStr.indexOf("COLOR") >= 0) { + pitch = 16 + fmt = "color" + } else { + pitch = 16 + fmt = "float4" + } + } + + buffer_descriptions.push({ + slot: i, + pitch: pitch, + input_rate: "vertex", + instance_step_rate: 0, + name:inp.name.split(".").pop() + }) + + attributes.push({ + location: inp.location, + buffer_slot: i, + format: fmt, + offset: 0 + }) + } + + pipeline.vertex_buffer_descriptions = buffer_descriptions + pipeline.vertex_attributes = attributes + + pipeline.gpu = context.make_pipeline(pipeline); +} + +make_pipeline[prosperon.DOC] = `Create and store a GPU pipeline object if it has not already been created. + +:param pipeline: An object describing the pipeline state, shaders, and reflection data. +:return: None +` + +var shader_type; + +function make_shader(sh_file) { + var file = `shaders/${shader_type}/${sh_file}.${shader_type}` + if (shader_cache[file]) return shader_cache[file] + var refl = json.decode(io.slurp(`shaders/reflection/${sh_file}.json`)) + + var shader = { + code: io.slurpbytes(file), + format: shader_type, + stage: sh_file.endsWith("vert") ? "vertex" : "fragment", + num_samplers: refl.separate_samplers ? refl.separate_samplers.length : 0, + num_textures: 0, + num_storage_buffers: refl.separate_storage_buffers ? refl.separate_storage_buffers.length : 0, + num_uniform_buffers: refl.ubos ? refl.ubos.length : 0, + entrypoint: shader_type === "msl" ? "main0" : "main" + } + + shader.gpu = context.make_shader(shader) + shader.reflection = refl; + shader_cache[file] = shader + shader.file = sh_file + return shader +} + +make_shader[prosperon.DOC] = `Load and compile a shader from disk, caching the result. Reflective metadata is also loaded. + +:param sh_file: The base filename (without extension) of the shader to compile. +:return: A shader object with GPU and reflection data attached. +` + +var render_queue = []; +var hud_queue = []; + +var current_queue = render_queue; + +var std_sampler = { + min_filter: "nearest", + mag_filter: "nearest", + mipmap: "linear", + u: "repeat", + v: "repeat", + w: "repeat", + mip_bias: 0, + max_anisotropy: 0, + compare_op: "none", + min_lod: 0, + max_lod: 0, + anisotropy: false, + compare: false +}; + +function upload_model(model) { + var bufs = []; + for (var i in model) { + if (typeof model[i] !== 'object') continue; + bufs.push(model[i]); + } + context.upload(this, bufs); +} + +upload_model[prosperon.DOC] = `Upload all buffer-like properties of the given model to the GPU. + +:param model: An object whose buffer properties are to be uploaded. +:return: None +` + +function bind_model(pass, pipeline, model) { + var buffers = pipeline.vertex_buffer_descriptions; + var bufs = []; + if (buffers) + for (var b of buffers) { + if (b.name in model) bufs.push(model[b.name]) + else throw Error (`could not find buffer ${b.name} on model`); + } + pass.bind_buffers(0,bufs); + pass.bind_index_buffer(model.indices); +} + +bind_model[prosperon.DOC] = `Bind the model's vertex and index buffers for the given pipeline and render pass. + +:param pass: The current render pass. +:param pipeline: The pipeline object with vertex buffer descriptions. +:param model: The model object containing matching buffers and an index buffer. +:return: None +` + +function bind_mat(pass, pipeline, mat) { + var imgs = []; + var refl = pipeline.fragment.reflection; + if (refl.separate_images) { + for (var i of refl.separate_images) { + if (i.name in mat) { + var tex = mat[i.name]; + imgs.push({texture:tex.texture, sampler:tex.sampler}); + } else + throw Error (`could not find all necessary images: ${i.name}`) + } + pass.bind_samplers(false, 0,imgs); + } +} + +bind_mat[prosperon.DOC] = `Bind the material images and samplers needed by the pipeline's fragment shader. + +:param pass: The current render pass. +:param pipeline: The pipeline whose fragment shader reflection indicates required textures. +:param mat: An object mapping the required image names to {texture, sampler}. +:return: None +` + +function group_sprites_by_texture(sprites, mesh) { + if (sprites.length === 0) return; + for (var i = 0; i < sprites.length; i++) { + sprites[i].mesh = mesh; + sprites[i].first_index = i*6; + sprites[i].num_indices = 6; + } + return; + // The code below is an alternate approach to grouping by image. Currently not in use. + /* + var groups = []; + var group = {image:sprites[0].image, first_index:0}; + var count = 1; + for (var i = 1; i < sprites.length; i++) { + if (sprites[i].image === group.image) { + count++; + continue; + } + group.num_indices = count*6; + var newgroup = {image:sprites[i].image, first_index:group.first_index+group.num_indices}; + group = newgroup; + groups.push(group); + count=1; + } + group.num_indices = count*6; + return groups; + */ +} + +group_sprites_by_texture[prosperon.DOC] = `Assign each sprite to the provided mesh, generating index data as needed. + +:param sprites: An array of sprite objects. +:param mesh: A mesh object (pos, color, uv, indices, etc.) to link to each sprite. +:return: None +` + +var main_color = { + type:"2d", + format: "rgba8", + layers: 1, + mip_levels: 1, + samples: 0, + sampler:true, + color_target:true +}; + +var main_depth = { + type: "2d", + format: "d32 float s8", + layers:1, + mip_levels:1, + samples:0, + sampler:true, + depth_target:true +}; + +function render_camera(cmds, camera) { + var pass; + delete camera.target // TODO: HORRIBLE + if (!camera.target) { + main_color.width = main_depth.width = camera.size.x; + main_color.height = main_depth.height = camera.size.y; + camera.target = { + color_targets: [{ + texture: context.texture(main_color), + mip_level:0, + layer: 0, + load:"clear", + store:"store", + clear: cornflower + }], + depth_stencil: { + texture: context.texture(main_depth), + clear:1, + load:"dont_care", + store:"dont_care", + stencil_load:"dont_care", + stencil_store:"dont_care", + stencil_clear:0 + } + }; + } + + var buffers = []; + buffers = buffers.concat(graphics.queue_sprite_mesh(render_queue)); + var unique_meshes = [...new Set(render_queue.map(x => x.mesh))]; + for (var q of unique_meshes) + buffers = buffers.concat([q.pos, q.color, q.uv, q.indices]); + + buffers = buffers.concat(graphics.queue_sprite_mesh(hud_queue)); + for (var q of hud_queue) + if (q.type === 'geometry') buffers = buffers.concat([q.mesh.pos, q.mesh.color, q.mesh.uv, q.mesh.indices]); + + full_upload(buffers) + + var pass = cmds.render_pass(camera.target); + + var pipeline = sprite_pipeline; + bind_pipeline(pass,pipeline); + + var camslot = get_pipeline_ubo_slot(pipeline, 'TransformBuffer'); + if (typeof camslot !== 'undefined') + cmds.camera(camera, camslot); + + modelslot = get_pipeline_ubo_slot(pipeline, "model"); + if (typeof modelslot !== 'undefined') { + var ubo = ubo_obj_to_array(pipeline, 'model', sprite_model_ubo); + cmds.push_vertex_uniform_data(modelslot, ubo); + } + + var mesh; + var img; + var modelslot; + + cmds.push_debug_group("draw") + for (var group of render_queue) { + if (mesh != group.mesh) { + mesh = group.mesh; + bind_model(pass,pipeline,mesh); + } + + if (group.image && img != group.image) { + img = group.image; + img.sampler = std_sampler; + bind_mat(pass,pipeline,{diffuse:img}); + } + + pass.draw_indexed(group.num_indices, 1, group.first_index, 0, 0); + } + cmds.pop_debug_group() + + cmds.push_debug_group("hud") + var camslot = get_pipeline_ubo_slot(pipeline, 'TransformBuffer'); + if (typeof camslot !== 'undefined') + cmds.hud(camera.size, camslot); + + for (var group of hud_queue) { + if (mesh != group.mesh) { + mesh = group.mesh; + bind_model(pass,pipeline,mesh); + } + + if (group.image && img != group.image) { + img = group.image; + img.sampler = std_sampler; + bind_mat(pass,pipeline,{diffuse:img}); + } + + pass.draw_indexed(group.num_indices, 1, group.first_index, 0, 0); + } + cmds.pop_debug_group(); + + pass?.end(); + + render_queue = []; + hud_queue = []; +} + +render_camera[prosperon.DOC] = `Render a scene using the provided camera, drawing both render queue and HUD queue items. + +:param cmds: A command buffer obtained from the GPU context. +:param camera: The camera object (with size, optional target, etc.). +:return: None +` + +var swaps = []; +render.present = function() { + os.clean_transforms(); + var cmds = context.acquire_cmd_buffer(); + render_camera(cmds, prosperon.camera); + var swapchain_tex = cmds.acquire_swapchain(); + if (!swapchain_tex) + cmds.cancel(); + else { + var torect = prosperon.camera.draw_rect(prosperon.window.size); + torect.texture = swapchain_tex; + if (swapchain_tex) { + cmds.blit({ + src: prosperon.camera.target.color_targets[0].texture, + dst: torect, + filter:"nearest", + load: "clear" + }); + + if (imgui) { // draws any imgui commands present + cmds.push_debug_group("imgui") + imgui.prepend(cmds); + var pass = cmds.render_pass({ + color_targets:[{texture:swapchain_tex}]}); + imgui.endframe(cmds,pass); + pass.end(); + cmds.pop_debug_group() + } + } + cmds.submit() + } +} + +render.present[prosperon.DOC] = `Perform the per-frame rendering and present the final swapchain image, including imgui pass if available. + +:return: None +` + +var stencil_write = { + compare: "always", + fail_op: "replace", + depth_fail_op: "replace", + pass_op: "replace" +}; + +function stencil_writer(ref) { + var pipe = Object.create(base_pipeline); + Object.assign(pipe, { + stencil: { + enabled: true, + front: stencil_write, + back: stencil_write, + write:true, + read:true, + ref:ref + }, + write_mask: colormask.none + }); + return pipe; +}.hashify(); + +// objects by default draw where the stencil buffer is 0 +function fillmask(ref) { + var pipe = stencil_writer(ref); + render.use_shader('screenfill.cg', pipe); + render.draw(shape.quad); +} + +render.fillmask[prosperon.DOC] = `Draw a fullscreen shape using a 'screenfill' shader to populate the stencil buffer with a given reference. + +:param ref: The stencil reference value to write. +:return: None +` + +var stencil_invert = { + compare: "always", + fail_op: "invert", + depth_fail_op: "invert", + pass_op: "invert" +}; + +function mask(image, pos, scale, rotation = 0, ref = 1) { + if (typeof image === 'string') + image = graphics.texture(image); + + var tex = image.texture; + if (scale) scale = scale.div([tex.width,tex.height]); + else scale = [1,1,1] + + var pipe = stencil_writer(ref); + render.use_shader('sprite.cg', pipe); + var t = os.make_transform(); + t.trs(pos, undefined, scale); + set_model(t); + render.use_mat({ + diffuse:image.texture, + rect: image.rect, + shade: Color.white + }); + render.draw(shape.quad); +} + +render.mask[prosperon.DOC] = `Draw an image to the stencil buffer, marking its area with a specified reference value. + +:param image: A texture or string path (which is converted to a texture). +:param pos: The translation (x, y) for the image placement. +:param scale: Optional scaling applied to the texture. +:param rotation: Optional rotation in radians (unused by default). +:param ref: The stencil reference value to write. +:return: None +` + +render.viewport = function(rect) { + context.viewport(rect); +} + +render.viewport[prosperon.DOC] = `Set the GPU viewport to the specified rectangle. + +:param rect: A rectangle [x, y, width, height]. +:return: None +` + +render.scissor = function(rect) { + render.viewport(rect) +} + +render.scissor[prosperon.DOC] = `Set the GPU scissor region to the specified rectangle (alias of render.viewport). + +:param rect: A rectangle [x, y, width, height]. +:return: None +` + +var std_sampler + +if (tracy) tracy.gpu_init() + +render.queue = function(cmd) { + if (Array.isArray(cmd)) + for (var i of cmd) current_queue.push(i) + else + current_queue.push(cmd) +} + +render.queue[prosperon.DOC] = `Enqueue one or more draw commands. These commands are batched until render_camera is called. + +:param cmd: Either a single command object or an array of command objects. +:return: None +` + +render.setup_draw = function() { + current_queue = render_queue; + prosperon.draw(); +} + +render.setup_draw[prosperon.DOC] = `Switch the current queue to the primary scene render queue, then invoke 'prosperon.draw' if defined. + +:return: None +` + +render.setup_hud = function() { + current_queue = hud_queue; + prosperon.hud(); +} + +render.setup_hud[prosperon.DOC] = `Switch the current queue to the HUD render queue, then invoke 'prosperon.hud' if defined. + +:return: None +` + +render.initialize = function(config) +{ + var default_conf = { + title:`Prosperon [${prosperon.version}-${prosperon.revision}]`, + width: 1280, + height: 720, + icon: graphics.make_texture(io.slurpbytes('icons/moon.gif')), + high_dpi:0, + alpha:1, + fullscreen:0, + sample_count:1, + enable_clipboard:true, + enable_dragndrop: true, + max_dropped_files: 1, + swap_interval: 1, + name: "Prosperon", + version:prosperon.version + "-" + prosperon.revision, + identifier: "world.pockle.prosperon", + creator: "Pockle World LLC", + copyright: "Copyright Pockle World 2025", + type: "game", + url: "https://prosperon.dev" + } + + config.__proto__ = default_conf + + prosperon.camera = use('ext/camera').make() + prosperon.camera.size = [config.width,config.height] + + prosperon.window = prosperon.engine_start(config) + + context = prosperon.window.make_gpu(false,driver) + context.window = prosperon.window + context.claim_window(prosperon.window) + context.set_swapchain('sdr', 'vsync') + + if (imgui) imgui.init(context, prosperon.window) + + shader_type = context.shader_format()[0]; + + std_sampler = context.make_sampler({ + min_filter: "nearest", + mag_filter: "nearest", + mipmap_mode: "nearest", + address_mode_u: "repeat", + address_mode_v: "repeat", + address_mode_w: "repeat" + }); +} + +return render + diff --git a/scripts/modules/sdl_render.js b/scripts/modules/sdl_render.js index 314982df..9f9e58de 100644 --- a/scripts/modules/sdl_render.js +++ b/scripts/modules/sdl_render.js @@ -1,15 +1,42 @@ var render = {} +var graphics = use('graphics') + var context render.initialize = function(config) { - prosperon.window = prosperon.engine_start(config) + var default_conf = { + title:`Prosperon [${prosperon.version}-${prosperon.revision}]`, + width: 1280, + height: 720, + icon: graphics.make_texture(io.slurpbytes('icons/moon.gif')), + high_dpi:0, + alpha:1, + fullscreen:0, + sample_count:1, + enable_clipboard:true, + enable_dragndrop: true, + max_dropped_files: 1, + swap_interval: 1, + name: "Prosperon", + version:prosperon.version + "-" + prosperon.revision, + identifier: "world.pockle.prosperon", + creator: "Pockle World LLC", + copyright: "Copyright Pockle World 2025", + type: "game", + url: "https://prosperon.dev" + } + +// for (var i in default_conf) +// config[i] ??= default_conf[i] + + prosperon.window = prosperon.engine_start({width:500,height:500}) context = prosperon.window.make_renderer() } // img here is the engine surface -render.load_texture(img) +render.load_texture = function(img) { if (!img.surface) throw new Error('Image must have a surface.') diff --git a/source/jsffi.c b/source/jsffi.c index fbf09b9f..6e030064 100644 --- a/source/jsffi.c +++ b/source/jsffi.c @@ -2878,7 +2878,7 @@ JSC_CCALL(renderer_scale, JSC_CCALL(renderer_vsync, SDL_Renderer *r = js2SDL_Renderer(js,self); - SDL_SetRenderVSync(r,js2number(js,argv[0])); + return JS_NewBool(js, SDL_SetRenderVSync(r,js2number(js,argv[0]))); ) // This returns the coordinates inside the diff --git a/tests/camera.js b/tests/camera.js new file mode 100644 index 00000000..775d37f5 --- /dev/null +++ b/tests/camera.js @@ -0,0 +1,15 @@ +var render = use('sdl_render') +console.log("HERE") +render.initialize({}) + +var draw2d = use('draw2d') + +var camera = { + x: 0, + y: 0, + zoom: 1, + surface: undefined, + viewport: {x:0,y:0,width:1,height:1} +} + +$_.delay($_.stop, 3)