Files
cell/prosperon/prosperon.cm

833 lines
20 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 zoom = camera.zoom;
// Use surface dimensions if rendering to a surface, otherwise window dimensions
def cw = camera.surface ? camera.surface.width : win_size.width;
def ch = camera.surface ? camera.surface.height : win_size.height;
// how big is the world window?
def world_w = cw / zoom;
def world_h = ch / zoom;
// compute worldspace bounds so that camera.pos lands at the anchor
// anchor.x = 0 → origin at left; 1 → origin at right
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;
// now build the Metal/Vulkanstyle ortho (z ∈ [0,1])
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 device = new sdl_gpu.gpu({
shaders_msl:true,
shaders_metallib:true,
name: "metal"
})
device.claim_window(window)
device.set_swapchain(window, 'sdr', 'vsync')
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
}
var usepipe
function load_pipeline(config)
{
if (usepipe) return usepipe
config.vertex = make_shader(config.vertex)[GPU]
config.fragment = make_shader(config.fragment)[GPU]
usepipe = new sdl_gpu.graphics_pipeline(device, config)
log.console(usepipe)
return usepipe
}
// 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) {
// Transform mouse coordinates from window to renderer coordinates
if (event.pos && (event.type == 'mouse_motion' ||
event.type == 'mouse_button_down' ||
event.type == 'mouse_button_up' ||
event.type == 'mouse_wheel')) {
// Convert window coordinates to renderer logical coordinates
// var logicalPos = renderer.coordsFromWindow(event.pos)
// event.pos = logicalPos
}
// Handle drop events which also have position
if (event.pos && (event.type == 'drop_file' ||
event.type == 'drop_text' ||
event.type == 'drop_position')) {
// var logicalPos = renderer.coordsFromWindow(event.pos)
// event.pos = logicalPos
}
// Handle window events
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
// event.pos.y = -event.pos.y + logical.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 pipey = load_pipeline(sprite_pipeline)
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);
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)
{
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:pipey,
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
render_geom(geom, img)
}
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
)
render_geom(mesh, font)
}
cmd_fns.tilemap = function(cmd)
{
var geometryCommands = cmd.tilemap.draw()
for (var geomCmd of geometryCommands) {
var img = graphics.texture(geomCmd.image)
if (!img) continue
render_geom(geomCmd.geometry, img)
}
}
cmd_fns.geometry = function(cmd)
{
if (typeof cmd.image == 'object') {
render_geom(cmd.geometry, cmd.image)
return
}
var img = graphics.texture(cmd.image)
if (!img) return
render_geom(cmd.geometry, img)
}
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)
render_geom(mesh, img)
}
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
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)
} 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",
}]
})
}
render_pass.bind_pipeline(pipey)
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
);
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
})
cur_cam = make_camera_pblob(cmd.camera)
render_queue.push_vertex_uniform_data(0, cur_cam)
continue
}
// 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)
}
}
}
})
// Schedule next poll in 0.5 seconds
$_.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