diff --git a/AGENTS.md b/AGENTS.md index 5c2fe7d0..0cc33649 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,26 +1,52 @@ -# AGENTS.md +# Agent Development Guide -## Project Overview -This is a game engine developed using a QuickJS fork as its scripting language. It is an actor based system, based on Douglas Crockford's Misty. It is a Meson compiled project with a number of dependencies. +## Build Commands +- **Debug build**: `make debug` or `meson setup build_dbg -Dbuildtype=debugoptimized && meson compile -C build_dbg` +- **Release build**: `make release` or `meson setup -Dbuildtype=release -Db_lto=true build_release && meson compile -C build_release` +- **Fast build**: `make fast` or `meson setup build_fast && meson compile -C build_fast` +- **Web build**: `make web` or `meson setup -Deditor=false -Dbuildtype=minsize build_web && meson compile -C build_web` +- **Cross-platform**: `make crosswin` (Windows), `make dockerlinux` (Linux), `make dockeremc` (Emscripten) -## File Structure -- `source/`: Contains the C source code -- `scripts/`: Contains script code that is loaded on executable start, and modules -- `shaders/`: Contains shaders that ship with the engine (for shader based backends) -- `benchmarks/`: Benchmark programs for testing speed -- `tests/`: Unit tests -- `examples/`: Contains full game examples +## Test Commands +- **Run all tests**: `meson test -C build_dbg` +- **Run single test**: `./cell tests/` (e.g., `./cell tests/send`, `./cell tests/empty`) +- **Available tests**: spawn_actor, empty, nota, wota, portalspawner, overling, send, delay +- **Test examples**: `./cell examples/nat`, `./cell examples/http_download_actor` -## Coding Practices -- Use K&R style C -- Javascript style prefers objects and prototypical inheritence over ES6 classes, liberal use of closures, and var everywhere +## Code Style Guidelines -## Instructions -- When generating code, adhere to the coding practices outlined above. -- When adding new features, ensure they align with the project's goals. -- When fixing bugs, review the code carefully before making changes. -- When writing unit tests, cover all important scenarios. +### C Code Style (K&R) +- **Indentation**: 4 spaces, no tabs +- **Naming**: `snake_case` for identifiers +- **Files**: Small, focused C files with header guards required +- **Formatting**: Use `.clang-format` (GNU style, 2-space continuation, attach braces) +- **Comments**: Focus on "why", keep lines to ~100 columns -## Compiling, running, and testing -- To compile the code, run "make", which generates a prosperon executable in build_dbg/, and copy it into the root folder -- Run a test by giving it as its command: so ./prosperon tests/overling.js would run the test overling.js, ./prosperon tests/nota.js runs the nota benchmark +### Cell Language Style (JavaScript-like) +- **Indentation**: 2 spaces +- **Declarations**: `def` for constants, `var` for block-scoped variables (like `let`) +- **Equality**: Use `==` only (strict equality, no coercion) +- **Null checks**: Use `== null` (no `undefined` in Cell) +- **Imports**: Use `use('path')` for modules (not ES6 import/export) +- **Modules**: `*.cm` files must return values, `*.ce` files are programs (don't return) +- **Patterns**: Prefer objects, prototypes, closures over classes +- **Formatting**: Use `.prettierrc` (semicolons, double quotes, trailing commas, 1000 char width) + +### File Organization +- **Modules**: `*.cm` (return objects, get frozen with `stone()`) +- **Programs**: `*.ce` (execute top-to-bottom, register handlers) +- **Naming**: Lowercase paths with `/` separators +- **Resolution**: Engine appends `.cm`/`.ce` automatically when probing + +### Error Handling +- Use `log.console()` and `log.error()` for logging +- Actor system handles message timeouts automatically +- Check for circular imports (detected and reported) +- Validate module returns before freezing + +### Best Practices +- Keep modules small and composable +- Use actor messaging for communication (no shared objects) +- Follow hierarchical actor system (overlings/underlings) +- Prefer functional programming patterns +- Document with `cell.DOC` system \ No newline at end of file diff --git a/docs/.pages b/docs/.pages index d0dda36d..116b777b 100644 --- a/docs/.pages +++ b/docs/.pages @@ -1,5 +1,6 @@ nav: - index.md + - quickstart.md - tutorial.md - actors.md - rendering.md @@ -9,4 +10,4 @@ nav: - ... - Appendix A - dull: dull - Appendix B - api: api - \ No newline at end of file + diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 00000000..87a862bf --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,133 @@ +# Quickstart + +This quickstart walks through running Cell programs, exploring examples, and creating your first module and program. It assumes the `./cell` executable is present at the repo root (already built or downloaded). + +## 1) Initialize a Project Shop (.cell) + +Create the project structure used for module management and bytecode cache: + +- `./cell init` + +This creates `.cell/` with `cell.toml`, `lock.toml`, `modules/`, `build/`, and `patches/`. + +## 2) Run Built‑in Examples + +Examples under `examples/` are programs (`*.ce`) you can run directly: + +- NAT portal server (introduction service): + - `./cell examples/nat` +- NAT client (contacts a portal): + - `./cell examples/nat_client` +- Non‑blocking HTTP download actor: + - `./cell examples/http_download_actor` + +Tip: Use these as references for portals, contacts, and non‑blocking I/O. + +## 3) Run the Accio Game + +Accio lives under `accio/`. The program is `accio/accio.ce` and supports modes via arguments: + +- Start menu: `./cell accio start` +- Load a level: `./cell accio level game/1.json` +- Analyze assets: `./cell accio analyze` +- Clean level data: `./cell accio clean` + +Arguments after the program path are available inside the program via `arg` (e.g., `arg[0] == 'start'`). + +## 4) Your First Module (.cm) and Program (.ce) + +Create a module that returns a frozen API object, and a program that uses it. + +- File: `examples/hello.cm` (module) + +```javascript +// Returns a value (frozen by the engine) +return { + greet: function(name) { + return `Hello, ${name}!` + } +} +``` + +- File: `examples/hello.ce` (program) + +```javascript +var hello = use('examples/hello') + +log.console(hello.greet('Cell')) + +$_.receiver(function(msg) { + if (msg.type == 'ping') { + send(msg, {type:'pong'}) + } +}) + +$_.delay(_ => $_.stop(), 0.1) +``` + +Run it: + +- `./cell examples/hello` + +Notes: +- Modules are `*.cm` and must return a value. The engine deep‑freezes return values, so mutate via new objects or closures rather than in‑place. +- Programs are `*.ce` and must not return a value. They run top‑to‑bottom when spawned and can register handlers via `$_.receiver()` and schedule work via `$_.delay()` or `$_.clock()`. + +## 5) Spawning Child Programs (Actors) + +Programs can spawn other programs and receive lifecycle events. + +- File: `examples/spawner.ce` + +```javascript +$_.receiver(function(e) { + if (e.type == 'greet' && e.actor) { + log.console('Child greeted me') + } +}) + +$_.start(function(info) { + if (info.type == 'greet') { + log.console('Spawned child actor') + } +}, 'examples/hello.ce') + +$_.delay(_ => $_.stop(), 0.5) +``` + +Run it: + +- `./cell examples/spawner` + +## 6) Module Shop Basics + +The module shop manages vendored dependencies under `.cell/modules/` and caches compiled bytecode under `.cell/build/`. + +Common commands (all are programs under `scripts/`): + +- Initialize (if you haven’t already): + - `./cell init` +- See available commands: + - `./cell help` +- Configure system/actor settings: + - `./cell config list` + - `./cell config set system.reply_timeout 60` + - `./cell config actor prosperon/_sdl_video set resolution 1280x720` +- Download or vendor dependencies (from `.cell/cell.toml`): + - `./cell mod download` + +## 7) How Module Resolution Works + +- `use('path')` checks: + 1) The current module’s directory for `path.cm` while loading. + 2) The mounted roots (the program’s directory is mounted) for `path.cm`. + 3) Embedded/native modules if no script file is found. +- Modules compile to `.cell/build/.o` and reuse the cache if newer than the source. +- Scripted modules can extend embedded modules via prototype; if the script returns nothing, the embedded module is used as‑is. + +## 8) Next Steps + +- Language details: `docs/cell.md` +- Actors, programs, and messaging: `docs/actors.md` +- Rendering, input, and resources: `docs/rendering.md`, `docs/input.md`, `docs/resources.md` +- Full API reference: `docs/api/` diff --git a/prosperon/_sdl_gpu.cm b/prosperon/_sdl_gpu.cm new file mode 100644 index 00000000..540a7fe5 --- /dev/null +++ b/prosperon/_sdl_gpu.cm @@ -0,0 +1,655 @@ +var render = {} + +var io = use('io') +var controller = use('controller') +var tracy = use('tracy') +var graphics = use('graphics') +var imgui = use('imgui') +var transform = use('transform') +var color = use('color') + +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", + 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(); +} + +function bind_pipeline(pass, pipeline) { + make_pipeline(pipeline) + pass.bind_pipeline(pipeline.gpu) + pass.pipeline = pipeline; +} + +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 null; +} + +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; +} + +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; +} + +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); + } +} + +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); +} + +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 +} + +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); +} + +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); +} + +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); + } +} + +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; + */ +} + +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 (camslot != null) + cmds.camera(camera, camslot); + + modelslot = get_pipeline_ubo_slot(pipeline, "model"); + if (modelslot != null) { + 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 (camslot != null) + 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 = []; +} + +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() + } +} + +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); +} + +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, null, scale); + set_model(t); + render.use_mat({ + diffuse:image.texture, + rect: image.rect, + shade: color.white + }); + render.draw(shape.quad); +} + +render.viewport = function(rect) { + context.viewport(rect); +} + +render.scissor = function(rect) { + render.viewport(rect) +} + +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.setup_draw = function() { + current_queue = render_queue; + prosperon.draw(); +} + +render.setup_hud = function() { + current_queue = hud_queue; + prosperon.hud(); +} + +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/prosperon/prosperon_sdl_renderer.cm b/prosperon/prosperon_sdl_renderer.cm new file mode 100644 index 00000000..7620fbe8 --- /dev/null +++ b/prosperon/prosperon_sdl_renderer.cm @@ -0,0 +1,502 @@ +var prosperon = {} + +// This file is hard coded for the SDL renderer case + +var video = use('sdl_video') +var imgui = use('imgui') +var surface = use('surface') + +var default_window = { + // Basic properties + title: "Prosperon Window", + width: 640, + height: 480, + + // 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 (let 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 + +var window = new video.window(win_config) +var renderer = window.make_renderer() + +// Initialize ImGui with the window and renderer +imgui.init(window, renderer) +imgui.newframe() + +var os = use('os'); +var io = use('io'); +var rasterize = use('rasterize'); +var time = use('time') +var tilemap = use('tilemap') +var geometry = use('geometry') +var res = use('resources') +var input = use('input') + +var graphics = use('graphics') + +var camera = {} + +function updateCameraMatrix(cam) { + def win_w = logical.width + def win_h = logical.height + def view_w = (cam.size?.[0] ?? win_w) / cam.zoom + def view_h = (cam.size?.[1] ?? win_h) / cam.zoom + + def ox = cam.pos[0] - view_w * (cam.anchor?.[0] ?? 0) + def oy = cam.pos[1] - view_h * (cam.anchor?.[1] ?? 0) + + def vx = (cam.viewport?.x ?? 0) * win_w + def vy = (cam.viewport?.y ?? 0) * win_h + def vw = (cam.viewport?.width ?? 1) * win_w + def vh = (cam.viewport?.height ?? 1) * win_h + + def sx = vw / view_w + def sy = vh / view_h // flip-Y later + + /* affine matrix that SDL wants (Y going down) */ + cam.a = sx + cam.c = vx - sx * ox + cam.e = -sy // <-- minus = flip Y + cam.f = vy + vh + sy * oy + + /* convenience inverses */ + cam.ia = 1 / cam.a + cam.ic = -cam.c / cam.a + cam.ie = 1 / cam.e + cam.if = -cam.f / cam.e + + camera = cam +} + +//---- forward transform ---- +function worldToScreenPoint([x,y], camera) { + return { + x: camera.a * x + camera.c, + y: camera.e * y + camera.f + }; +} + +//---- inverse transform ---- +function screenToWorldPoint(pos, camera) { + return { + x: camera.ia * pos[0] + camera.ic, + y: camera.ie * pos[1] + camera.if + }; +} + +//---- rectangle (two corner) ---- +function worldToScreenRect({x,y,width,height}, camera) { + // map bottom-left and top-right + def x1 = camera.a * x + camera.c; + def y1 = camera.e * y + camera.f; + def x2 = camera.a * (x + width) + camera.c; + def y2 = camera.e * (y + height) + camera.f; + + return { + x:Math.min(x1,x2), + y:Math.min(y1,y2), + width:Math.abs(x2-x1), + height:Math.abs(y2-y1) + } +} + +var gameactor + +var images = {} + +var renderer_commands = [] + +var win_size = {width:500,height:500} +var logical = {width:500,height:500} + +function get_img_gpu(img) +{ + if (img.gpu) return img.gpu + var surf = new surface(img.cpu) + img.gpu = renderer.load_texture(surf) + return img.gpu +} + +// Convert high-level draw commands to low-level renderer commands +function translate_draw_commands(commands) { + renderer_commands.length = 0 + + commands.forEach(function(cmd) { + if (cmd.material && cmd.material.color && typeof cmd.material.color == 'object') { + renderer.drawColor = cmd.material.color + } + switch(cmd.cmd) { + case "camera": + updateCameraMatrix(cmd.camera, win_size.width, win_size.height) + break + + case "draw_rect": + cmd.rect = worldToScreenRect(cmd.rect, camera) + renderer.fillRect(cmd.rect) + break + + case "draw_line": + var points = cmd.points.map(p => { + var pt = worldToScreenPoint(p, camera) + return[pt.x, pt.y] + }) + renderer.line(points) + break + + case "draw_point": + cmd.pos = worldToScreenPoint(cmd.pos, camera) + renderer.point(cmd.pos) + break + + case "draw_image": + var img = graphics.texture(cmd.image) + var gpu = get_img_gpu(img) + + if (!cmd.scale) cmd.scale = {x:1,y:1} + cmd.rect.width ??= img.width + cmd.rect.height ??= img.height + cmd.rect.width = cmd.rect.width * cmd.scale.x + cmd.rect.height = cmd.rect.height * cmd.scale.y + cmd.rect = worldToScreenRect(cmd.rect, camera) + renderer.texture( + gpu, + img.rect, + cmd.rect, + 0, + {x:0,y:0} + ) + + break + + case "draw_slice9": + var img = graphics.texture(cmd.image) + var gpu = get_img_gpu(img) + if (!gpu) break + var rect = worldToScreenRect(cmd.rect, camera) + renderer.texture9Grid( + gpu, + img.rect, + cmd.slice, + cmd.slice, + cmd.slice, + cmd.slice, + 1, + rect + ) + break + + case "draw_text": + if (!cmd.text) break + if (!cmd.pos) break + + // Get font from the font string (e.g., "smalle.16") + var font = graphics.get_font(cmd.font) + if (!font.gpu) { + var surf = new surface(font.surface) + font.gpu = renderer.load_texture(surf) + } + var gpu = font.gpu + + // Create text geometry buffer + var text_mesh = graphics.make_text_buffer( + cmd.text, + {x: cmd.pos.x, y: cmd.pos.y}, + [cmd.material.color.r, cmd.material.color.g, cmd.material.color.b, cmd.material.color.a], + cmd.wrap || 0, + font + ) + + if (!text_mesh) break + + if (text_mesh.xy.length == 0) break + + // Transform XY coordinates using camera matrix + var camera_params = [camera.a, camera.c, camera.e, camera.f] + var transformed_xy = geometry.transform_xy_blob(text_mesh.xy, camera_params) + + // Create transformed geometry object + var geom = { + xy: transformed_xy, + xy_stride: text_mesh.xy_stride, + uv: text_mesh.uv, + uv_stride: text_mesh.uv_stride, + color: text_mesh.color, + color_stride: text_mesh.color_stride, + indices: text_mesh.indices, + num_vertices: text_mesh.num_vertices, + num_indices: text_mesh.num_indices, + size_indices: text_mesh.size_indices + } + + renderer.geometry_raw(gpu, geom.xy, geom.xy_stride, geom.color, geom.color_stride, geom.uv, geom.uv_stride, geom.num_vertices, geom.indices, geom.num_indices, geom.size_indices) + break + + case "tilemap": + var geometryCommands = cmd.tilemap.draw() + + for (var geomCmd of geometryCommands) { + var img = graphics.texture(geomCmd.image) + if (!img) continue + + if (!img.gpu) { + var surf = new surface(img.cpu) + img.gpu = renderer.load_texture(surf) + } + var gpu = img.gpu + + var geom = geomCmd.geometry + + var camera_params = [camera.a, camera.c, camera.e, camera.f] + var transformed_xy = geometry.transform_xy_blob(geom.xy, camera_params) + + renderer.geometry_raw(gpu, transformed_xy, geom.xy_stride, geom.color, geom.color_stride, geom.uv, geom.uv_stride, geom.num_vertices, geom.indices, geom.num_indices, geom.size_indices) + } + break + + case "geometry": + var gpu = get_img_gpu(cmd.image) + log.console(json.encode(cmd)) + var geom = cmd.geometry + + var camera_params = [camera.a, camera.c, camera.e, camera.f] + var transformed_xy = geometry.transform_xy_blob(geom.xy, camera_params) + + renderer.geometry_raw(gpu, transformed_xy, geom.xy_stride, geom.color, geom.color_stride, geom.uv, geom.uv_stride, geom.num_vertices, geom.indices, geom.num_indices, geom.size_indices) + break + } + }) + + return renderer_commands +} + +///// input ///// +var input_cb +var input_rate = 1/60 +function poll_input() { + var evs = input.get_events() + + // Filter and transform events + if (renderer && Array.isArray(evs)) { + var filteredEvents = [] + var wantMouse = imgui.wantmouse() + var wantKeys = imgui.wantkeys() + + 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 + logical.height + + filteredEvents.push(event) + } + } + + evs = filteredEvents + } + + input_cb(evs) + $_.delay(poll_input, input_rate) +} + +prosperon.input = function(fn) +{ + input_cb = fn + poll_input() +} + +// 2) helper to build & send a batch, then call done() +prosperon.create_batch = function create_batch(draw_cmds, done) { + renderer.drawColor = {r:0.1,g:0.1,b:0.15,a:1} + renderer.clear() + + if (draw_cmds && draw_cmds.length) + var commands = translate_draw_commands(draw_cmds) + + renderer.drawColor = {r:1,g:1,b:1,a:1} + imgui.endframe(renderer) + imgui.newframe() + + renderer.present() + + 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]) +} + +var renderer_cmds = { + resolution(e) { + logical.width = e.width + logical.height = e.height + renderer.logicalPresentation = {...e} + } +} + +prosperon.set_renderer = function(config) +{ + for (var c in config) + if (renderer_cmds[c]) renderer_cmds[c](config[c]) +} + +prosperon.init = function() { + // No longer needed since we initialize directly +} + +// Function to load textures directly to the renderer +prosperon.load_texture = function(surface_data) { + var surf = new surface(surface_data) + if (!surf) return null + + var tex = renderer.load_texture(surf) + if (!tex) return null + + // Set pixel mode to nearest for all textures + tex.scaleMode = "nearest" + + var tex_id = allocate_id() + resources.texture[tex_id] = tex + + return { + id: tex_id, + texture: tex, + width: tex.width, + height: tex.height + } +} + +return prosperon diff --git a/scripts/graphics.cm b/scripts/graphics.cm index 4acf83c3..ca15a74c 100644 --- a/scripts/graphics.cm +++ b/scripts/graphics.cm @@ -276,14 +276,9 @@ graphics.texture = function texture(path) { height: Math.floor(img_32k.height / 8), mode: 'linear' }) - log.console(img_32k.pixels.length) - log.console(surf_4k.pixels.length) - log.console(json.encode(surf_4k)) - var qoi_data = surface.compress_qoi(surf_4k) + var qoi_data = qoi.encode(surf_4k) - log.console(json.encode(qoi_data)) - if (qoi_data && qoi_data.pixels) { if (!io.exists(cache_dir)) { io.mkdir(cache_dir) diff --git a/source/nota.h b/source/nota.h index c9ff68bb..12e2e119 100755 --- a/source/nota.h +++ b/source/nota.h @@ -5,7 +5,6 @@ #include #include "kim.h" -/* Nota type nibble values */ #define NOTA_BLOB 0x00 #define NOTA_TEXT 0x10 #define NOTA_ARR 0x20 @@ -21,7 +20,6 @@ #define NOTA_PRIVATE 0x08 #define NOTA_SYSTEM 0x09 -/* Some internal constants/macros (used in varint logic, etc.) */ #define NOTA_CONT 0x80 #define NOTA_DATA 0x7f #define NOTA_INT_DATA 0x07 @@ -33,7 +31,6 @@ #define CONTINUE(CHAR) (CHAR>>7) #define UTF8_DATA 0x3f -/* A helper to get the high-level Nota type nibble from a byte */ static inline int nota_type(const char *nota) { return (*nota) & 0x70; } char *nota_read_blob(long long *len, char **blob, char *nota); @@ -50,10 +47,8 @@ typedef struct NotaBuffer { size_t capacity; /* allocated size of data */ } NotaBuffer; -/* Initialize a NotaBuffer with a given initial capacity. */ void nota_buffer_init(NotaBuffer *nb, size_t initial_capacity); -/* Free the buffer's internal memory. (Does NOT free nb itself.) */ void nota_buffer_free(NotaBuffer *nb); void nota_write_blob (NotaBuffer *nb, unsigned long long nbits, const char *data); @@ -73,25 +68,18 @@ void nota_write_sym (NotaBuffer *nb, int sym); #include "kim.h" -/* ------------------------------------------------------- - HELPER: skip a varint - ------------------------------------------------------- */ static inline char *nota_skip(char *nota) { - while (CONTINUE(*nota)) { - nota++; - } + while (CONTINUE(*nota)) + nota++; return nota + 1; } -/* ------------------------------------------------------- - HELPER: read a varint - ------------------------------------------------------- */ char *nota_read_num(long long *n, char *nota) { - if (!n) { - return nota_skip(nota); - } + if (!n) + return nota_skip(nota); + unsigned char b = (unsigned char)*nota; long long result = b & NOTA_HEAD_DATA; nota++; @@ -106,7 +94,7 @@ char *nota_read_num(long long *n, char *nota) } /* Count how many bits of varint we need to encode n, - with sb “special bits” in the first byte. */ + with sb “special bits” in the first byte */ static inline int nota_bits(long long n, int sb) { if (n == 0) return sb; @@ -116,7 +104,6 @@ static inline int nota_bits(long long n, int sb) return needed; } -/* Write a varint into *nota, with sb bits in the first char (which is already set). */ static inline char *nota_continue_num(long long n, char *nota, int sb) { int bits = nota_bits(n, sb); @@ -272,7 +259,6 @@ void nota_buffer_free(NotaBuffer *nb) nb->capacity = 0; } -/* Allocate 'len' bytes in the buffer and return a pointer to them. */ static char *nota_buffer_alloc(NotaBuffer *nb, size_t len) { nota_buffer_grow(nb, len); @@ -363,183 +349,11 @@ void nota_write_record(NotaBuffer *nb, unsigned long long count) nb->size -= (10 - used); } -void nota_write_number_str(NotaBuffer *nb, const char *str) -{ - /* ------------------------------------------- - 1) Parse sign - ------------------------------------------- */ - int negative = 0; - if (*str == '+') { - str++; - } - else if (*str == '-') { - negative = 1; - str++; - } - - /* ------------------------------------------- - 2) Parse integer part - ------------------------------------------- */ - long long coefficient = 0; - int got_digits = 0; - - while (*str >= '0' && *str <= '9') { - got_digits = 1; - int d = (*str - '0'); - str++; - - // Basic overflow check (very naive): - if (coefficient <= (LLONG_MAX - d) / 10) { - coefficient = coefficient * 10 + d; - } else { - // If you want to handle overflow by switching to float, do that here. - // For simplicity, let's just keep wrapping. In production, be careful! - coefficient = coefficient * 10 + d; - } - } - - /* ------------------------------------------- - 3) Check for decimal part - ------------------------------------------- */ - int has_decimal_point = 0; - int fraction_digits = 0; - - if (*str == '.') { - has_decimal_point = 1; - str++; - while (*str >= '0' && *str <= '9') { - got_digits = 1; - int d = (*str - '0'); - str++; - fraction_digits++; - if (coefficient <= (LLONG_MAX - d) / 10) { - coefficient = coefficient * 10 + d; - } else { - // Same naive overflow comment - coefficient = coefficient * 10 + d; - } - } - } - - /* ------------------------------------------- - 4) Check for exponent part - ------------------------------------------- */ - int exponent_negative = 0; - long long exponent_val = 0; - - if (*str == 'e' || *str == 'E') { - str++; - if (*str == '+') { - str++; - } - else if (*str == '-') { - exponent_negative = 1; - str++; - } - while (*str >= '0' && *str <= '9') { - int d = (*str - '0'); - str++; - if (exponent_val <= (LLONG_MAX - d) / 10) { - exponent_val = exponent_val * 10 + d; - } else { - // Again, naive overflow handling - exponent_val = exponent_val * 10 + d; - } - } - } - - /* ------------------------------------------- - 5) If there were no valid digits at all, - store 0 and return. (simple fallback) - ------------------------------------------- */ - if (!got_digits) { - nota_write_int_buf(nb, 0); - return; - } - - /* ------------------------------------------- - 6) Combine fraction digits into exponent - final_exponent = exponent_val - fraction_digits - (apply exponent sign if any) - ------------------------------------------- */ - if (exponent_negative) { - exponent_val = -exponent_val; - } - long long final_exponent = exponent_val - fraction_digits; - - /* ------------------------------------------- - 7) Decide if we are storing an integer - or a float in Nota format. - ------------------------------------------- - Rule used here: - - If there's no decimal point AND final_exponent == 0, - => integer - - If we do have a decimal point, but fraction_digits == 0 - and exponent_val == 0, then the user typed something - like "123." or "100.0". That is effectively an integer, - so store it as an integer if you want a purely numeric approach. - - Otherwise store as float. - ------------------------------------------- */ - - // If "no decimal" => definitely integer: - // or decimal present but fraction_digits=0 & exponent_val=0 => integer - int treat_as_integer = 0; - if (!has_decimal_point && final_exponent == 0) { - treat_as_integer = 1; - } - else if (has_decimal_point && fraction_digits == 0 && exponent_val == 0) { - // Means "123." or "123.0" - treat_as_integer = 1; - } - - if (treat_as_integer) { - // If negative => flip the sign in the stored value - if (negative) { - coefficient = -coefficient; - } - // Write the integer in Nota format (varint with sign bit) - nota_write_int_buf(nb, coefficient); - return; - } - - /* ------------------------------------------- - 8) Write as float in Nota format - We do basically the same approach as - nota_write_float_buf does: - - NOTA_FLOAT nibble - - sign bit if negative - - exponent sign bit if final_exponent < 0 - - varint of |final_exponent| - - varint of |coefficient| - ------------------------------------------- */ - { - char *p = nota_buffer_alloc(nb, 21); // Up to ~21 bytes worst-case - p[0] = NOTA_FLOAT; - if (negative) { - p[0] |= (1 << 3); // Mantissa sign bit - } - if (final_exponent < 0) { - p[0] |= (1 << 4); // Exponent sign bit - final_exponent = -final_exponent; - } - // Write exponent as varint (with 3 bits used in the first byte) - char *c = nota_continue_num(final_exponent, p, 3); - // Write the absolute coefficient (7 bits used in the first byte) - char *end = nota_continue_num(coefficient, c, 7); - - // Adjust the buffer size to the actual used length - size_t used = (size_t)(end - p); - nb->size -= (21 - used); - } -} - - void nota_write_number(NotaBuffer *nb, double n) { nota_write_int_or_float_buf(nb, n); } -/* Write an integer in varint form (with sign bit) */ static void nota_write_int_buf(NotaBuffer *nb, long long n) { /* up to ~10 bytes for varint */ diff --git a/source/qjs_nota.c b/source/qjs_nota.c index dae1a7a6..a3218cca 100755 --- a/source/qjs_nota.c +++ b/source/qjs_nota.c @@ -146,19 +146,13 @@ static void nota_encode_value(NotaEncodeContext *enc, JSValueConst val, JSValueC int tag = JS_VALUE_GET_TAG(replaced); switch (tag) { - case JS_TAG_INT: { + case JS_TAG_INT: + case JS_TAG_FLOAT64: { double d; JS_ToFloat64(ctx, &d, replaced); nota_write_number(&enc->nb, d); break; } - case JS_TAG_BIG_INT: - case JS_TAG_FLOAT64: { - const char *str = JS_ToCString(ctx, replaced); - nota_write_number_str(&enc->nb, str); - JS_FreeCString(ctx, str); - break; - } case JS_TAG_STRING: { const char *str = JS_ToCString(ctx, replaced); nota_write_text(&enc->nb, str);