Files
cell/prosperon/prosperon.cm
2025-08-01 06:35:25 -05:00

921 lines
22 KiB
Plaintext

var prosperon = {}
// This file is hard coded for the SDL GPU case
var video = use('sdl_video')
var surface = use('surface')
var sdl_gpu = use('sdl_gpu')
var io = use('io')
var geometry = use('geometry')
var blob = use('blob')
var os = use('os')
var win_size = {width:500,height:500}
function makeOrthoMetal(l,r,b,t,n,f){
return [
2/(r-l), 0, 0, 0,
0, 2/(t-b), 0, 0,
0, 0, 1/(f-n), 0,
-(r+l)/(r-l), -(t+b)/(t-b), -n/(f-n), 1
]
}
function make_camera_pblob(camera) {
def cw = camera.surface ? camera.surface.width : win_size.width;
def ch = camera.surface ? camera.surface.height : win_size.height;
def zoom = camera.zoom || ch
def world_h = zoom;
def world_w = zoom * cw / ch;
def l = camera.pos[0] - camera.anchor[0] * world_w;
def b = camera.pos[1] - camera.anchor[1] * world_h;
def r = l + world_w;
def t = b + world_h;
def mat = makeOrthoMetal(l, r, b, t, 0, 1);
return geometry.array_blob(mat);
}
var driver = "vulkan"
switch(os.platform()) {
case "Linux":
driver = "vulkan"
break
case "Windows":
// driver = "direct3d12"
driver = "vulkan"
break
case "macOS":
driver = "metal"
break
}
var default_sampler = {
min_filter: "linear",
mag_filter: "linear",
mipmap: "nearest",
u: "repeat",
v: "repeat",
w: "repeat",
mip_bias: 0,
max_anisotropy: 0,
compare_op: "none",
min_lod: 0,
max_lod: 10,
anisotropy: false,
compare: false
};
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
};
var default_window = {
// Basic properties
title: "Prosperon Window",
width: 640,
height: 360,
// Position - can be numbers or "centered"
x: null, // SDL_WINDOWPOS_null by default
y: null, // SDL_WINDOWPOS_null by default
// Window behavior flags
resizable: true,
fullscreen: false,
hidden: false,
borderless: false,
alwaysOnTop: false,
minimized: false,
maximized: false,
// Input grabbing
mouseGrabbed: false,
keyboardGrabbed: false,
// Display properties
highPixelDensity: false,
transparent: false,
opacity: 1.0, // 0.0 to 1.0
// Focus behavior
notFocusable: false,
// Special window types (mutually exclusive)
utility: false, // Utility window (not in taskbar)
tooltip: false, // Tooltip window (requires parent)
popupMenu: false, // Popup menu window (requires parent)
// Graphics API flags (var SDL choose if not specified)
opengl: false, // Force OpenGL context
vulkan: false, // Force Vulkan context
metal: false, // Force Metal context (macOS)
// Advanced properties
parent: null, // Parent window for tooltips/popups/modal
modal: false, // Modal to parent window (requires parent)
externalGraphicsContext: false, // Use external graphics context
// Input handling
textInput: true, // Enable text input on creation
}
var win_config = arg[0] || {}
win_config.__proto__ = default_window
win_config.metal = true
var window = new video.window(win_config)
var win_proto = window.__proto__
win_proto.toJSON = function()
{
var flags = this.flags
var ret = {
title: this.title,
size: this.size,
pixel_size: this.sizeInPixels,
display_scale: this.displayScale,
pixel_density: this.pixelDensity,
pos: this.position,
opacity: this.opacity,
fullscreen: this.fullscreen,
safe_area: this.safe_area(),
}
for (var i in flags)
ret[i] = flags[i]
return ret
}
window.resizable = true
var device = new sdl_gpu.gpu({
shaders_msl:true,
shaders_metallib:true,
name: "metal"
})
device.claim_window(window)
device.set_swapchain(window, 'sdr', 'vsync')
var white_pixel = {
width:1,
height:1,
pixels: new blob(32, true), // 32 bits, all set to 1 for a white blob
pitch:32
}
stone(white_pixel.pixels)
var shader_type = device.shader_format()[0]
shader_type = 'msl'
var sampler_cache = {}
function canonicalize_sampler(desc) {
if (desc == true)
return json.encode(default_sampler)
var sampler_obj = {}
sampler_obj.__proto__ = default_sampler
if (typeof desc == 'object') {
for (var key in desc) {
if (desc.hasOwnProperty(key)) {
sampler_obj[key] = desc[key]
}
}
}
var keys = Object.keys(sampler_obj).sort()
var canonical = {}
for (var i = 0; i < keys.length; i++)
canonical[keys[i]] = sampler_obj[keys[i]]
return json.encode(canonical)
}
function get_sampler(desc) {
var key = canonicalize_sampler(desc)
if (!sampler_cache[key]) {
var sampler_config = json.decode(key)
sampler_cache[key] = new sdl_gpu.sampler(device, sampler_config)
}
return sampler_cache[key]
}
var std_sampler = get_sampler(true)
// Shader and pipeline cache
var shader_cache = {}
var pipeline_cache = {}
function upload(copypass, buffer, toblob)
{
stone(toblob)
var trans = new sdl_gpu.transfer_buffer(device, {
size: toblob.length/8,
usage:"upload"
})
trans.copy_blob(device, toblob)
copypass.upload_to_buffer({
transfer_buffer: trans,
offset:0
}, {
buffer: buffer,
offset: 0,
size: toblob.length/8
})
}
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] = new sdl_gpu.shader(device, shader)
shader.reflection = refl;
shader_cache[file] = shader
shader.file = sh_file
return shader
}
def material_pipeline_cache = {};
function get_pipeline_for_material(mat = {}) {
def key = json.encode({
vert: mat.vertex || sprite_pipeline.vertex,
frag: mat.fragment || sprite_pipeline.fragment,
blend: mat.blend || sprite_pipeline.blend,
cull: mat.cull || sprite_pipeline.cull,
});
if (!material_pipeline_cache[key]) {
def cfg = Object.assign({}, sprite_pipeline, {
vertex: mat.vertex || sprite_pipeline.vertex,
fragment: mat.fragment || sprite_pipeline.fragment,
blend: mat.blend || sprite_pipeline.blend,
cull: mat.cull || sprite_pipeline.cull,
});
cfg.__proto__ = sprite_pipeline
material_pipeline_cache[key] = load_pipeline(cfg)
log.console(`created pipeline for ${json.encode(cfg)}`)
}
return material_pipeline_cache[key];
}
function load_pipeline(config)
{
config.vertex = make_shader(config.vertex)[GPU]
config.fragment = make_shader(config.fragment)[GPU]
return new sdl_gpu.graphics_pipeline(device, config)
}
// Initialize ImGui with the window and renderer
//imgui.init(window, renderer)
//imgui.newframe()
var io = use('io');
var rasterize = use('rasterize');
var time = use('time')
var tilemap = use('tilemap')
var res = use('resources')
var input = use('input')
var graphics = use('graphics')
var camera = {}
prosperon.scissor = function(rect) {
device.scissor(rect)
}
// Pipeline component definitions
var default_depth_state = {
compare: "always", // never/less/equal/less_equal/greater/not_equal/greater_equal/always
test: false,
write: false,
bias: 0,
bias_slope_scale: 0,
bias_clamp: 0
}
var default_stencil_state = {
compare: "always", // 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"
}
var disabled_blend_state = {
enabled: false,
src_rgb: "zero",
dst_rgb: "zero",
op_rgb: "add",
src_alpha: "one",
dst_alpha: "zero",
op_alpha: "add"
}
var alpha_blend_state = {
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"
}
var default_multisample_state = {
count: 1,
mask: 0xFFFFFFFF,
domask: false
}
// Helper function to create pipeline config
function create_pipeline_config(options) {
var config = {
vertex: options.vertex,
fragment: options.fragment,
primitive: options.primitive || "triangle",
fill: options.fill ?? true,
depth: options.depth || default_depth_state,
stencil: {
enabled: options.stencil_enabled ?? false,
front: options.stencil_front || default_stencil_state,
back: options.stencil_back || default_stencil_state,
test: options.stencil_test ?? false,
compare_mask: options.stencil_compare_mask ?? 0,
write_mask: options.stencil_write_mask ?? 0
},
blend: options.blend || disabled_blend_state,
cull: options.cull || "none",
face: options.face || "cw",
alpha_to_coverage: options.alpha_to_coverage ?? false,
multisample: options.multisample || default_multisample_state,
label: options.label || "pipeline",
target: options.target || {}
}
// Ensure target has required properties
if (!config.target.color_targets) {
config.target.color_targets = [{
format: "rgba8",
blend: config.blend
}]
}
return config
}
var gameactor
var images = {}
var renderer_commands = []
///// input /////
var input_cb
var input_rate = 1/60
function poll_input() {
var evs = input.get_events()
// Filter and transform events
if (Array.isArray(evs)) {
var filteredEvents = []
// var wantMouse = imgui.wantmouse()
// var wantKeys = imgui.wantkeys()
var wantMouse = false
var wantKeys = false
for (var i = 0; i < evs.length; i++) {
var event = evs[i]
var shouldInclude = true
// Filter mouse events if ImGui wants mouse input
if (wantMouse && (event.type == 'mouse_motion' ||
event.type == 'mouse_button_down' ||
event.type == 'mouse_button_up' ||
event.type == 'mouse_wheel')) {
shouldInclude = false
}
// Filter keyboard events if ImGui wants keyboard input
if (wantKeys && (event.type == 'key_down' ||
event.type == 'key_up' ||
event.type == 'text_input' ||
event.type == 'text_editing')) {
shouldInclude = false
}
if (shouldInclude) {
if (event.type == 'window_pixel_size_changed') {
win_size.width = event.width
win_size.height = event.height
}
if (event.type == 'quit')
$_.stop()
if (event.type.includes('key')) {
if (event.key)
event.key = input.keyname(event.key)
}
if (event.type.startsWith('mouse_') && event.pos && event.pos.y)
event.pos.y = -event.pos.y + win_size.height
filteredEvents.push(event)
}
}
evs = filteredEvents
}
input_cb(evs)
$_.delay(poll_input, input_rate)
}
prosperon.input = function(fn)
{
input_cb = fn
poll_input()
}
var sprite_pipeline = {
vertex: "sprite.vert",
fragment: "sprite.frag",
cull: "none",
target: {
color_targets: [
{format: device.swapchain_format(window), blend:alpha_blend_state}
],
},
vertex_buffer_descriptions: [ { slot:0, input_rate: "vertex", instance_step_rate: 0,
pitch: 8},
{slot:1, input_rate:"vertex", instance_step_rate: 0, pitch: 8},
{slot:2, input_rate:"vertex", instance_step_rate: 0, pitch: 16}
],
vertex_attributes: [
{ location: 0, buffer_slot: 0, format: "float2", offset: 0},
{ location: 1, buffer_slot: 1, format: "float2", offset: 0},
{ location: 2, buffer_slot: 2, format: "float4", offset: 0}
],
primitive: "triangle",
blend: alpha_blend_state
}
var GPU = Symbol()
var cur_cam
var cmd_fns = {}
cmd_fns.camera = function(cmd)
{
if (cmd.camera.surface && !cmd.camera.surface[GPU]) {
cmd.camera.surface[GPU] = new sdl_gpu.texture(device, cmd.camera.surface)
// Store the sampler description on the texture for later use
if (cmd.camera.surface.sampler != null) {
cmd.camera.surface[GPU].sampler_desc = cmd.camera.surface.sampler
}
}
draw_queue.push(cmd)
}
var new_tex = []
function get_img_gpu(surface)
{
if (!surface) return
var full_mip = Math.floor(Math.log2(Math.max(surface.width,surface.height))) + 1
var gpu = new sdl_gpu.texture(device, {
width: surface.width,
height: surface.height,
layers: 1,
mip_levels: full_mip,
samples: 0,
type: "2d",
format: "rgba8",
sampler: surface.sampler != null ? surface.sampler : true,
color_target: true
})
// Store the sampler description on the texture for later use
if (surface.sampler != null) {
gpu.sampler_desc = surface.sampler
}
var tbuf = new sdl_gpu.transfer_buffer(device, {
size: surface.pixels.length/8,
usage: "upload"
})
tbuf.copy_blob(device, surface.pixels)
copy_pass.upload_to_texture({
transfer_buffer: tbuf,
offset: 0,
pixels_per_row: surface.width,
rows_per_layer: surface.height,
}, {
texture: gpu,
mip_level: 0,
layer: 0,
x: 0, y: 0, z: 0,
w: surface.width,
h: surface.height,
d: 1
}, false);
if (full_mip > 1)
new_tex.push(gpu)
return gpu
}
var pos_blob
var uv_blob
var color_blob
var index_blob
var draw_queue = []
var index_count = 0
var vertex_count = 0
function render_geom(geom, img, pipeline = get_pipeline_for_material(null))
{
if (!img[GPU]) {
if (img.surface)
img[GPU] = get_img_gpu(img.surface)
else
img[GPU] = get_img_gpu(img.cpu)
if (!img[GPU]) return
}
pos_blob.write_blob(geom.xy)
uv_blob.write_blob(geom.uv)
color_blob.write_blob(geom.color)
index_blob.write_blob(geom.indices)
draw_queue.push({
pipeline,
texture: img[GPU],
num_indices: geom.num_indices,
first_index: index_count,
vertex_offset: vertex_count
})
vertex_count += (geom.xy.length/8) / 8
index_count += geom.num_indices
}
cmd_fns.draw_image = function(cmd)
{
var img
if (typeof cmd.image == 'string')
img = graphics.texture(cmd.image)
else
img = cmd.image
cmd.rect.width ??= img.width
cmd.rect.height ??= img.height
var geom = geometry.make_rect_quad(cmd.rect)
geom.indices = geometry.make_quad_indices(1)
geom.num_indices = 6
var pipeline = get_pipeline_for_material(cmd.material)
render_geom(geom, img, pipeline)
}
cmd_fns.draw_text = function(cmd)
{
if (!cmd.text || !cmd.pos) return
var font = graphics.get_font(cmd.font)
if (!font[GPU])
font[GPU] = get_img_gpu(font.surface)
var mesh = graphics.make_text_buffer(
cmd.text,
cmd.pos,
[cmd.material.color.r, cmd.material.color.g, cmd.material.color.b, cmd.material.color.a],
cmd.wrap || 0,
font
)
var pipeline = get_pipeline_for_material(cmd.material)
render_geom(mesh, font, pipeline)
}
cmd_fns.tilemap = function(cmd)
{
var geometryCommands = cmd.tilemap.draw()
var pipeline = get_pipeline_for_material(cmd.material)
for (var geomCmd of geometryCommands) {
var img = graphics.texture(geomCmd.image)
if (!img) continue
render_geom(geomCmd.geometry, img, pipeline)
}
}
cmd_fns.geometry = function(cmd)
{
var pipeline = get_pipeline_for_material(cmd.material)
if (typeof cmd.image == 'object') {
render_geom(cmd.geometry, cmd.image, pipeline)
return
}
var img = graphics.texture(cmd.image)
if (!img) return
render_geom(cmd.geometry, img, pipeline)
}
cmd_fns.draw_slice9 = function(cmd)
{
var img = graphics.texture(cmd.image)
if (!img) return
// Use the gpu_slice9 function from geometry module to generate the mesh
var slice_info = {
tile_top: true,
tile_bottom: true,
tile_left: true,
tile_right: true,
tile_center_x: true,
tile_center_y: true
}
// Convert single slice value to LRTB object if needed
var slice_lrtb = cmd.slice
if (typeof cmd.slice == 'number') {
slice_lrtb = {
l: cmd.slice,
r: cmd.slice,
t: cmd.slice,
b: cmd.slice
}
}
var mesh = geometry.slice9(img, cmd.rect, slice_lrtb, slice_info)
var pipeline = get_pipeline_for_material(cmd.material)
render_geom(mesh, img, pipeline)
}
cmd_fns.draw_rect = function(cmd)
{
// Create geometry for a rectangle quad
var geom = geometry.make_rect_quad(cmd.rect, null, cmd.material.color)
geom.indices = geometry.make_quad_indices(1)
geom.num_indices = 6
// Use white_pixel as the texture so the color modulation works
if (!white_pixel[GPU])
white_pixel[GPU] = get_img_gpu(white_pixel)
var pipeline = get_pipeline_for_material(cmd.material)
render_geom(geom, {[GPU]: white_pixel[GPU]}, pipeline)
}
var copy_pass
prosperon.create_batch = function create_batch(draw_cmds, done) {
pos_blob = new blob
uv_blob = new blob
color_blob = new blob
index_blob = new blob
draw_queue = []
index_count = 0
vertex_count = 0
new_tex = []
var render_queue = device.acquire_cmd_buffer()
copy_pass = render_queue.copy_pass()
for (var cmd of draw_cmds)
if (cmd_fns[cmd.cmd])
cmd_fns[cmd.cmd](cmd)
var pos_buffer = new sdl_gpu.buffer(device,{ vertex:true, size:pos_blob.length/8});
var uv_buffer = new sdl_gpu.buffer(device,{ vertex:true, size:uv_blob.length/8});
var color_buffer = new sdl_gpu.buffer(device,{ vertex:true, size:color_blob.length/8});
var index_buffer = new sdl_gpu.buffer(device,{ index:true, size:index_blob.length/8});
upload(copy_pass, pos_buffer, pos_blob)
upload(copy_pass, uv_buffer, uv_blob)
upload(copy_pass, color_buffer, color_blob)
upload(copy_pass, index_buffer, index_blob)
copy_pass.end();
for (var g of new_tex)
render_queue.generate_mipmaps(g)
var render_pass
var render_target
// State tracking for optimization
var current_pipeline = null
var current_camera_blob = null
var buffers_bound = false
for (var cmd of draw_queue) {
if (cmd.camera) {
if (!cmd.camera.surface && render_target != "swap") {
if (render_pass)
render_pass.end()
render_target = "swap"
render_pass = render_queue.swapchain_pass(window)
// Reset state tracking when render pass changes
current_pipeline = null
buffers_bound = false
} else if (cmd.camera.surface && render_target != cmd.camera.surface) {
if (render_pass)
render_pass.end()
render_target = cmd.camera.surface
render_pass = render_queue.render_pass({
color_targets: [{
texture: cmd.camera.surface[GPU],
mip_level: 0,
layer: 0,
load: "clear",
clear_color: cmd.camera.background,
store: "store",
}]
})
// Reset state tracking when render pass changes
current_pipeline = null
buffers_bound = false
}
var vpW, vpH
if (render_target == "swap") {
vpW = win_size.width
vpH = win_size.height
} else {
vpW = render_target.width
vpH = render_target.height
}
render_pass.viewport({
x: cmd.camera.viewport.x*vpW,
y: cmd.camera.viewport.y * vpH,
width: cmd.camera.viewport.width * vpW,
height: cmd.camera.viewport.height * vpH
})
var new_cam_blob = make_camera_pblob(cmd.camera)
// Only update camera uniform if it changed
if (current_camera_blob != new_cam_blob) {
render_queue.push_vertex_uniform_data(0, new_cam_blob)
current_camera_blob = new_cam_blob
}
continue
}
// Only bind pipeline if it changed
if (current_pipeline != cmd.pipeline) {
render_pass.bind_pipeline(cmd.pipeline)
current_pipeline = cmd.pipeline
// When pipeline changes, we need to rebind buffers and uniforms
buffers_bound = false
current_camera_blob = null
}
// Only bind buffers if not already bound or pipeline changed
if (!buffers_bound) {
render_pass.bind_buffers(0, [
{ buffer: pos_buffer, offset: 0 },
{ buffer: uv_buffer, offset: 0 },
{ buffer: color_buffer, offset: 0 }
])
render_pass.bind_index_buffer(
{ buffer: index_buffer, offset: 0 }, // the binding itself is in bytes
16 // 16 = Uint32 indices
);
buffers_bound = true
}
// Rebind camera uniform if needed after pipeline change
if (!current_camera_blob && cur_cam) {
render_queue.push_vertex_uniform_data(0, cur_cam)
current_camera_blob = cur_cam
}
// Use texture's sampler if it has one, otherwise use standard sampler
var sampler_to_use = std_sampler
if (cmd.texture && cmd.texture.sampler_desc) {
sampler_to_use = get_sampler(cmd.texture.sampler_desc)
}
render_pass.bind_samplers(false, 0, [{texture:cmd.texture, sampler: sampler_to_use}])
render_pass.draw_indexed(
cmd.num_indices,
1,
cmd.first_index,
cmd.vertex_offset,
0
)
}
render_pass.end()
render_queue.submit()
if (done) done()
}
////////// dmon hot reload ////////
function poll_file_changes() {
dmon.poll(e => {
if (e.action == 'modify' || e.action == 'create') {
// Check if it's an image file
var ext = e.file.split('.').pop().toLowerCase()
var imageExts = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tga', 'webp', 'qoi', 'ase', 'aseprite']
if (imageExts.includes(ext)) {
// Try to find the full path for this image
var possiblePaths = [
e.file,
e.root + e.file,
res.find_image(e.file.split('/').pop().split('.')[0])
].filter(p => p)
for (var path of possiblePaths) {
graphics.tex_hotreload(path)
}
}
}
})
$_.delay(poll_file_changes, 0.5)
}
var dmon = use('dmon')
prosperon.dmon = function()
{
dmon.watch('.')
poll_file_changes()
}
var window_cmds = {
size(size) {
window.size = size
},
}
prosperon.set_window = function(config)
{
for (var c in config)
if (window_cmds[c]) window_cmds[c](config[c])
}
return prosperon