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 transform = use('transform') 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 = new 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 = new 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