diff --git a/meson.build b/meson.build index d4421bef..d6e9b525 100644 --- a/meson.build +++ b/meson.build @@ -138,7 +138,7 @@ deps += dependency('soloud', static:true) deps += dependency('libqrencode', static: true) sources = [] -src += ['anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c','render.c','simplex.c','spline.c', 'transform.c','prosperon.c', 'wildmatch.c', 'sprite.c', 'rtree.c', 'qjs_dmon.c', 'qjs_nota.c', 'qjs_enet.c', 'qjs_soloud.c', 'qjs_qr.c', 'qjs_wota.c', 'monocypher.c'] +src += ['anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c','render.c','simplex.c','spline.c', 'transform.c','prosperon.c', 'wildmatch.c', 'sprite.c', 'rtree.c', 'qjs_dmon.c', 'qjs_nota.c', 'qjs_enet.c', 'qjs_soloud.c', 'qjs_qr.c', 'qjs_wota.c', 'monocypher.c', 'qjs_blob.c', 'qjs_crypto.c', 'qjs_time.c'] # quirc src src += ['thirdparty/quirc/quirc.c', 'thirdparty/quirc/decode.c','thirdparty/quirc/identify.c', 'thirdparty/quirc/version_db.c'] diff --git a/scripts/core/engine.js b/scripts/core/engine.js index db5e2fde..7ab586f2 100644 --- a/scripts/core/engine.js +++ b/scripts/core/engine.js @@ -87,8 +87,6 @@ prosperon.PATH = [ "scripts/modules/ext/", ] - - // path is the path of a module or script to resolve var script_fn = function script_fn(path) { var parsed = {} @@ -225,8 +223,6 @@ console[prosperon.DOC] = { clear: "Clear console." } - - var BASEPATH = 'scripts/core/base.js' var script = io.slurp(BASEPATH) var fnname = "base" @@ -288,8 +284,6 @@ globalThis.use = function use(file) { return use_cache[file] } - - globalThis.json = use('json') var time = use('time') @@ -297,8 +291,9 @@ function parse_file(content, file) { if (!content) return {} if (!/^\s*---\s*$/m.test(content)) { var part = content.trim() - if (part.match(/return\s+[^;]+;?\s*$/)) + if (part.match(/return\s+[^;]+;?\s*$/)) { return { module: part } + } return { program: part } } var parts = content.split(/\n\s*---\s*\n/) @@ -420,7 +415,6 @@ function cant_kill() { actor.toString = function() { return this[FILE] } actor.spawn = function spawn(script, config) { - if (this[DEAD]) throw new Error("Attempting to spawn on a dead actor") var prog if (!script) { @@ -536,17 +530,12 @@ actor[UNDERLINGS] = new Set() globalThis.mixin("color") - - var DOCPATH = 'scripts/core/doc.js' var script = io.slurp(DOCPATH) var fnname = "doc" script = `(function ${fnname}() { ${script}; })` //js.eval(DOCPATH, script)() - - - /* When handling a message, the message appears like this: { @@ -949,6 +938,10 @@ function handle_message(msg) { case "greet": var greeter = greeters[msg.id] if (greeter) greeter({type: "actor_started", actor: create_actor(msg)}) + break; + default: + if (receive_fn) receive_fn(msg) + break; } }; diff --git a/scripts/core/io.js b/scripts/core/io.js index 9530a6f0..b0cda680 100644 --- a/scripts/core/io.js +++ b/scripts/core/io.js @@ -6,10 +6,11 @@ var subscribers = [] $_.receiver(e => { if (e.type === "subscribe") { if (!e.actor) throw Error('Got a subscribe message with no actor.'); + console.log('subscribing: ' + json.encode(e.actor)) subscribers.push(e.actor) - return + return; } - + for (var a of subscribers) - $_.send(a, e) + $_.send(a, e); }); diff --git a/scripts/modules/sdl_render.js b/scripts/modules/sdl_render.js new file mode 100644 index 00000000..f1d738f7 --- /dev/null +++ b/scripts/modules/sdl_render.js @@ -0,0 +1,841 @@ +var render = {} + +var io = use('io') +var os = use('os') +var controller = use('controller') +var tracy = use('tracy') +var graphics = use('graphics') + +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" +} + +var config = use('config.js') +config.__proto__ = default_conf + +prosperon.camera = use('ext/camera').make() +prosperon.camera.size = [config.width,config.height] + +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" +}; + +sprite_pipeline.target = { + color_targets: [{ + format:"rgba8", + blend:sprite_pipeline.blend + }], + depth: "d32 float s8" +}; + +var appy = {}; +appy.inputs = {}; +if (os.platform() === "macos") { + appy.inputs["S-q"] = os.exit; +} + +appy.inputs["M-f4"] = os.exit; + +controller.player[0].control(appy); + +prosperon.window = prosperon.engine_start(config); + +var driver = "vulkan" +switch(os.platform()) { + case "Linux": + driver = "vulkan" + break + case "Windows": +// driver = "direct3d12" + driver = "vulkan" + break + case "macOS": + driver = "metal" + break +} + +render._main = prosperon.window.make_gpu(false,driver) +prosperon.gpu = render._main +render._main.window = prosperon.window +render._main.claim_window(prosperon.window) +render._main.set_swapchain('sdr', 'vsync') + +var whiteimage = {} +whiteimage.surface = graphics.make_surface([1,1]) +whiteimage.surface.rect({x:0,y:0,width:1,height:1}, [1,1,1,1]) +whiteimage.texture = render._main.load_texture(whiteimage.surface) + +var imgui = use('imgui') +if (imgui) imgui.init(render._main, prosperon.window) + +var unit_transform = os.make_transform(); + +var cur = {}; +cur.images = []; +cur.samplers = []; + +var tbuffer; +function full_upload(buffers) { + var cmds = render._main.acquire_cmd_buffer(); + tbuffer = render._main.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 = render._main.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 = render._main.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 = render._main.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. +` + +// helpful render devices. width and height in pixels; diagonal in inches. +render.device = { + pc: { width: 1920, height: 1080 }, + macbook_m2: { width: 2560, height: 1664, diagonal: 13.6 }, + ds_top: { width: 400, height: 240, diagonal: 3.53 }, + ds_bottom: { width: 320, height: 240, diagonal: 3.02 }, + playdate: { width: 400, height: 240, diagonal: 2.7 }, + switch: { width: 1280, height: 720, diagonal: 6.2 }, + switch_lite: { width: 1280, height: 720, diagonal: 5.5 }, + switch_oled: { width: 1280, height: 720, diagonal: 7 }, + dsi: { width: 256, height: 192, diagonal: 3.268 }, + ds: { width: 256, height: 192, diagonal: 3 }, + dsixl: { width: 256, height: 192, diagonal: 4.2 }, + ipad_air_m2: { width: 2360, height: 1640, diagonal: 11.97 }, + iphone_se: { width: 1334, height: 750, diagonal: 4.7 }, + iphone_12_pro: { width: 2532, height: 1170, diagonal: 6.06 }, + iphone_15: { width: 2556, height: 1179, diagonal: 6.1 }, + gba: { width: 240, height: 160, diagonal: 2.9 }, + gameboy: { width: 160, height: 144, diagonal: 2.48 }, + gbc: { width: 160, height: 144, diagonal: 2.28 }, + steamdeck: { width: 1280, height: 800, diagonal: 7 }, + vita: { width: 960, height: 544, diagonal: 5 }, + psp: { width: 480, height: 272, diagonal: 4.3 }, + imac_m3: { width: 4480, height: 2520, diagonal: 23.5 }, + macbook_pro_m3: { width: 3024, height: 1964, diagonal: 14.2 }, + ps1: { width: 320, height: 240, diagonal: 5 }, + ps2: { width: 640, height: 480 }, + snes: { width: 256, height: 224 }, + gamecube: { width: 640, height: 480 }, + n64: { width: 320, height: 240 }, + c64: { width: 320, height: 200 }, + macintosh: { width: 512, height: 342 }, + gamegear: { width: 160, height: 144, diagonal: 3.2 } +}; + +render.device.doc = `Device resolutions given as [x,y,inches diagonal].`; + +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]); + } + render._main.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: render._main.texture(main_color), + mip_level:0, + layer: 0, + load:"clear", + store:"store", + clear: cornflower + }], + depth_stencil: { + texture: render._main.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 = []; +function gpupresent() { + os.clean_transforms(); + var cmds = render._main.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() + } +} + +gpupresent[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" +}; + +var stencil_writer = 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(); + +render.stencil_writer = stencil_writer; + +// objects by default draw where the stencil buffer is 0 +render.fillmask = 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" +}; + +render.mask = function mask(image, pos, scale, rotation = 0, ref = 1) { + if (typeof image === 'string') + image = graphics.texture(image); + + var tex = image.texture; + if (scale) scale = scale.div([tex.width,tex.height]); + else scale = [1,1,1] + + var pipe = stencil_writer(ref); + render.use_shader('sprite.cg', pipe); + var t = os.make_transform(); + t.trs(pos, undefined, scale); + set_model(t); + render.use_mat({ + diffuse:image.texture, + rect: image.rect, + shade: Color.white + }); + render.draw(shape.quad); +} + +render.mask[prosperon.DOC] = `Draw an image to the stencil buffer, marking its area with a specified reference value. + +:param image: A texture or string path (which is converted to a texture). +:param pos: The translation (x, y) for the image placement. +:param scale: Optional scaling applied to the texture. +:param rotation: Optional rotation in radians (unused by default). +:param ref: The stencil reference value to write. +:return: None +` + +render.viewport = function(rect) { + render._main.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 +` + +// Some initialization +shader_type = render._main.shader_format()[0]; + +std_sampler = render._main.make_sampler({ + min_filter: "nearest", + mag_filter: "nearest", + mipmap_mode: "nearest", + address_mode_u: "repeat", + address_mode_v: "repeat", + address_mode_w: "repeat" +}); + +render._main.present = gpupresent; + +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 +` + +return render diff --git a/source/jsffi.c b/source/jsffi.c index c4b8a73d..411772b6 100644 --- a/source/jsffi.c +++ b/source/jsffi.c @@ -1014,6 +1014,8 @@ void SDL_Window_free(JSRuntime *rt, SDL_Window *w) SDL_DestroyWindow(w); } + + void SDL_Renderer_free(JSRuntime *rt, SDL_Renderer *r) { SDL_DestroyRenderer(r); @@ -1044,9 +1046,7 @@ void SDL_GPUCommandBuffer_free(JSRuntime *rt, SDL_GPUCommandBuffer *c) { } -void SDL_Thread_free(JSRuntime *rt, SDL_Thread *t) -{ -} +QJSCLASS(SDL_Renderer,) void SDL_GPUComputePass_free(JSRuntime *rt, SDL_GPUComputePass *c) { } void SDL_GPUCopyPass_free(JSRuntime *rt, SDL_GPUCopyPass *c) { } @@ -1082,7 +1082,6 @@ QJSCLASSMARK(transform,) QJSCLASS(font,) QJSCLASS(datastream,) QJSCLASS(SDL_Window,) -QJSCLASS(SDL_Renderer,) QJSCLASS(SDL_Camera,) void SDL_Texture_free(JSRuntime *rt, SDL_Texture *t){ @@ -1100,7 +1099,6 @@ QJSCLASS(SDL_Surface, ) QJSCLASS(SDL_GPUDevice,) -QJSCLASS(SDL_Thread,) GPURELEASECLASS(Buffer) GPURELEASECLASS(ComputePipeline) @@ -5182,15 +5180,6 @@ static const JSCFunctionListEntry js_SDL_Surface_funcs[] = { MIST_FUNC_DEF(surface, dup, 0), }; -JSC_CCALL(thread_wait, - SDL_Thread *th = js2SDL_Thread(js,self); - SDL_WaitThread(th, NULL); -) - -static const JSCFunctionListEntry js_SDL_Thread_funcs[] = { - MIST_FUNC_DEF(thread,wait,0), -}; - JSC_CCALL(camera_frame, SDL_ClearError(); SDL_Camera *cam = js2SDL_Camera(js,self); @@ -5315,30 +5304,6 @@ JSC_SCALL(os_openurl, ret = JS_ThrowReferenceError(js, "unable to open url %s: %s\n", str, SDL_GetError()); ) -JSC_CCALL(time_now, - struct timeval ct; - gettimeofday(&ct, NULL); - return number2js(js,(double)ct.tv_sec+(double)(ct.tv_usec/1000000.0)); -) - -JSValue js_time_computer_dst(JSContext *js, JSValue self) { - time_t t = time(NULL); - return JS_NewBool(js,localtime(&t)->tm_isdst); -} - -JSValue js_time_computer_zone(JSContext *js, JSValue self) { - time_t t = time(NULL); - time_t local_t = mktime(localtime(&t)); - double diff = difftime(t, local_t); - return number2js(js,diff/3600); -} - -static const JSCFunctionListEntry js_time_funcs[] = { - MIST_FUNC_DEF(time, now, 0), - MIST_FUNC_DEF(time, computer_dst, 0), - MIST_FUNC_DEF(time, computer_zone, 0) -}; - JSC_SCALL(console_print, printf("%s", str); ) @@ -6854,6 +6819,7 @@ static const JSCFunctionListEntry js_os_funcs[] = { MIST_FUNC_DEF(os, register_actor, 2), MIST_FUNC_DEF(os, unneeded, 2), MIST_FUNC_DEF(os, destroy, 0), + MIST_FUNC_DEF(os, ioactor, 0), }; JSC_CCALL(js_dump_class, return js_get_object_class_distribution(js)) @@ -7165,224 +7131,9 @@ JSValue js_miniz_use(JSContext *js); JSValue js_tracy_use(JSContext *js); #endif -#include "monocypher.h" - -// randombytes.c - Minimal cross-platform CSPRNG shim (single file) -/* - Usage: - #include "randombytes.c" - - int main() { - uint8_t buffer[32]; - if (randombytes(buffer, sizeof(buffer)) != 0) { - // handle error - } - // buffer now has 32 cryptographically secure random bytes - } -*/ - -#include -#include - -#if defined(_WIN32) -// ------- Windows: use BCryptGenRandom ------- -#include -#include - -int randombytes(void *buf, size_t n) { - NTSTATUS status = BCryptGenRandom(NULL, (PUCHAR)buf, (ULONG)n, BCRYPT_USE_SYSTEM_PREFERRED_RNG); - return (status == 0) ? 0 : -1; -} - -#elif defined(__linux__) -// ------- Linux: try getrandom, fall back to /dev/urandom ------- -#include -#include -#include -#include - -// If we have a new enough libc and kernel, getrandom is available. -// Otherwise, we’ll do a /dev/urandom fallback. -#include - -static int randombytes_fallback(void *buf, size_t n) { - int fd = open("/dev/urandom", O_RDONLY); - if (fd < 0) return -1; - ssize_t r = read(fd, buf, n); - close(fd); - return (r == (ssize_t)n) ? 0 : -1; -} - -int randombytes(void *buf, size_t n) { -#ifdef SYS_getrandom - // Try getrandom(2) if available - ssize_t ret = syscall(SYS_getrandom, buf, n, 0); - if (ret < 0) { - // If getrandom is not supported or fails, fall back - if (errno == ENOSYS) { - return randombytes_fallback(buf, n); - } - return -1; - } - return (ret == (ssize_t)n) ? 0 : -1; -#else - // getrandom not available, just fallback - return randombytes_fallback(buf, n); -#endif -} - -#else -// ------- Other Unix: read from /dev/urandom ------- -#include -#include - -int randombytes(void *buf, size_t n) { - int fd = open("/dev/urandom", O_RDONLY); - if (fd < 0) return -1; - ssize_t r = read(fd, buf, n); - close(fd); - return (r == (ssize_t)n) ? 0 : -1; -} -#endif - -static inline void to_hex(const uint8_t *in, size_t in_len, char *out) -{ - static const char hexchars[] = "0123456789abcdef"; - for (size_t i = 0; i < in_len; i++) { - out[2*i ] = hexchars[(in[i] >> 4) & 0x0F]; - out[2*i + 1] = hexchars[ in[i] & 0x0F]; - } - out[2 * in_len] = '\0'; // null-terminate -} - -static inline int nibble_from_char(char c, uint8_t *nibble) -{ - if (c >= '0' && c <= '9') { *nibble = (uint8_t)(c - '0'); return 0; } - if (c >= 'a' && c <= 'f') { *nibble = (uint8_t)(c - 'a' + 10); return 0; } - if (c >= 'A' && c <= 'F') { *nibble = (uint8_t)(c - 'A' + 10); return 0; } - return -1; // invalid char -} - -static inline int from_hex(const char *hex, uint8_t *out, size_t out_len) -{ - for (size_t i = 0; i < out_len; i++) { - uint8_t hi, lo; - if (nibble_from_char(hex[2*i], &hi) < 0) return -1; - if (nibble_from_char(hex[2*i + 1], &lo) < 0) return -1; - out[i] = (uint8_t)((hi << 4) | lo); - } - return 0; -} - -// Convert a JSValue containing a 64-character hex string into a 32-byte array. -static inline void js2crypto(JSContext *js, JSValue v, uint8_t *crypto) -{ - size_t hex_len; - const char *hex_str = JS_ToCStringLen(js, &hex_len, v); - if (!hex_str) - return; - - if (hex_len != 64) { - JS_FreeCString(js, hex_str); - JS_ThrowTypeError(js, "js2crypto: expected 64-hex-char string"); - return; - } - - if (from_hex(hex_str, crypto, 32) < 0) { - JS_FreeCString(js, hex_str); - JS_ThrowTypeError(js, "js2crypto: invalid hex encoding"); - return; - } - - JS_FreeCString(js, hex_str); -} - -static inline JSValue crypto2js(JSContext *js, const uint8_t *crypto) -{ - char hex[65]; // 32*2 + 1 for null terminator - to_hex(crypto, 32, hex); - return JS_NewString(js, hex); -} - -JSC_CCALL(crypto_keypair, - ret = JS_NewObject(js); - - uint8_t public[32]; - uint8_t private[32]; - - randombytes(private,32); - - private[0] &= 248; - private[31] &= 127; - private[31] |= 64; - - crypto_x25519_public_key(public,private); - - JS_SetPropertyStr(js, ret, "public", crypto2js(js, public)); - JS_SetPropertyStr(js, ret, "private", crypto2js(js,private)); -) - -JSC_CCALL(crypto_shared, -{ - if (argc < 1 || !JS_IsObject(argv[0])) { - return JS_ThrowTypeError(js, "crypto.shared: expected an object argument"); - } - - JSValue obj = argv[0]; - - JSValue val_pub = JS_GetPropertyStr(js, obj, "public"); - if (JS_IsException(val_pub)) { - JS_FreeValue(js, val_pub); - return JS_EXCEPTION; - } - - JSValue val_priv = JS_GetPropertyStr(js, obj, "private"); - if (JS_IsException(val_priv)) { - JS_FreeValue(js, val_pub); - JS_FreeValue(js, val_priv); - return JS_EXCEPTION; - } - - uint8_t pub[32], priv[32]; - js2crypto(js, val_pub, pub); - js2crypto(js, val_priv, priv); - - JS_FreeValue(js, val_pub); - JS_FreeValue(js, val_priv); - - uint8_t shared[32]; - crypto_x25519(shared, priv, pub); - - ret = crypto2js(js, shared); -}) - -JSC_CCALL(crypto_random, -{ - // 1) Pull 64 bits of cryptographically secure randomness - uint64_t r; - if (randombytes(&r, sizeof(r)) != 0) { - // If something fails (extremely rare), throw an error - return JS_ThrowInternalError(js, "crypto.random: unable to get random bytes"); - } - - // 2) Convert r to a double in the range [0,1). - // We divide by (UINT64_MAX + 1.0) to ensure we never produce exactly 1.0. - double val = (double)r / ((double)UINT64_MAX + 1.0); - - // 3) Return that as a JavaScript number - ret = JS_NewFloat64(js, val); -}) - -static const JSCFunctionListEntry js_crypto_funcs[] = { - MIST_FUNC_DEF(crypto, keypair, 0), - MIST_FUNC_DEF(crypto, shared, 1), - MIST_FUNC_DEF(crypto, random, 0), -}; - MISTUSE(io) MISTUSE(os) MISTUSE(input) -MISTUSE(time) MISTUSE(math) MISTUSE(spline) MISTUSE(geometry) @@ -7392,7 +7143,10 @@ MISTUSE(util) MISTUSE(video) MISTUSE(camera) MISTUSE(debug) -MISTUSE(crypto) + +#include "qjs_crypto.h" +#include "qjs_time.h" +#include "qjs_blob.h" JSValue js_imgui_use(JSContext *js); @@ -7428,6 +7182,7 @@ void ffi_load(JSContext *js) arrput(rt->module_registry, MISTLINE(qr)); arrput(rt->module_registry, MISTLINE(wota)); arrput(rt->module_registry, MISTLINE(crypto)); + arrput(rt->module_registry, MISTLINE(blob)); #ifdef TRACY_ENABLE arrput(rt->module_registry, MISTLINE(tracy)); @@ -7443,17 +7198,19 @@ void ffi_load(JSContext *js) QJSCLASSPREP_FUNCS(rtree) QJSCLASSPREP_FUNCS(SDL_Window) QJSCLASSPREP_FUNCS(SDL_Surface) - QJSCLASSPREP_FUNCS(SDL_Thread) QJSCLASSPREP_FUNCS(SDL_Texture) - QJSCLASSPREP_FUNCS(SDL_Renderer) + QJSCLASSPREP_NO_FUNCS(SDL_Cursor) QJSCLASSPREP_FUNCS(SDL_Camera) + + QJSCLASSPREP_FUNCS(SDL_Renderer) + QJSCLASSPREP_FUNCS(SDL_GPUDevice) QJSCLASSPREP_FUNCS(SDL_GPUTexture) QJSCLASSPREP_FUNCS(SDL_GPUCommandBuffer) QJSCLASSPREP_FUNCS(SDL_GPURenderPass) QJSCLASSPREP_FUNCS(SDL_GPUComputePass) - QJSCLASSPREP_NO_FUNCS(SDL_Cursor) + QJSCLASSPREP_NO_FUNCS(SDL_GPUCopyPass) QJSCLASSPREP_NO_FUNCS(SDL_GPUFence) QJSCLASSPREP_NO_FUNCS(SDL_GPUTransferBuffer) diff --git a/source/prosperon.c b/source/prosperon.c index 41937a90..6ed8821a 100644 --- a/source/prosperon.c +++ b/source/prosperon.c @@ -474,6 +474,8 @@ void actor_free(prosperon_rt *actor) /* If still present, free each JSValue. */ for (int i = 0; i < arrlen(actor->events); i++) JS_FreeValue(js, actor->events[i]); + + printf("FREEIN ACTOR EVENTS\n"); arrfree(actor->events); JSRuntime *rt = JS_GetRuntime(js); @@ -1352,17 +1354,17 @@ int main(int argc, char **argv) queue_cond = SDL_CreateCondition(); actors_mutex = SDL_CreateMutex(); - /* Create the initial actor from the main command line. */ - char **margv = malloc(sizeof(char *) * argc); - for (int i = 0; i < argc; i++) margv[i] = strdup(argv[i]); - create_actor(argc, margv); - const char *io_script = "scripts/core/io.js"; char **io_argv = malloc(sizeof(char*)*2); io_argv[0] = strdup(argv[0]); io_argv[1] = strdup(io_script); io_actor = create_actor(2, io_argv); + /* Create the initial actor from the main command line. */ + char **margv = malloc(sizeof(char *) * argc); + for (int i = 0; i < argc; i++) margv[i] = strdup(argv[i]); + create_actor(argc, margv); + /* Start the thread that pumps ready actors, one per logical core. */ for (int i = 0; i < cores; i++) { char threadname[128]; @@ -1383,8 +1385,9 @@ int main(int argc, char **argv) if (event.type == queue_event) goto QUEUE; - WotaBuffer wb = event2wota(&event); - send_message(io_actor->id, wb.data); +// WotaBuffer wb = event2wota(&event); +// send_message(io_actor->id, wb.data); + continue; QUEUE: SDL_LockMutex(queue_mutex); @@ -1411,3 +1414,8 @@ int actor_exists(char *id) else return 1; } + +JSValue js_os_ioactor(JSContext *js, JSValue self, int argc, JSValue *argv) +{ + return JS_NewString(js, io_actor->id); +} diff --git a/source/prosperon.h b/source/prosperon.h index e8e0d868..8826ec9a 100644 --- a/source/prosperon.h +++ b/source/prosperon.h @@ -84,4 +84,6 @@ void set_actor_state(prosperon_rt *actor); int prosperon_mount_core(void); +JSValue js_os_ioactor(JSContext *js, JSValue self, int argc, JSValue *argv); + #endif diff --git a/source/qjs_blob.c b/source/qjs_blob.c new file mode 100644 index 00000000..a43f6290 --- /dev/null +++ b/source/qjs_blob.c @@ -0,0 +1,460 @@ +#include +#include +#include +#include "quickjs.h" +#include "qjs_blob.h" + +// ----------------------------------------------------------------------------- +// A simple blob structure that can be in two states: +// - antestone (mutable): writing is allowed +// - stone (immutable): reading is allowed +// +// The blob is stored as an array of bits in memory, but for simplicity here, +// we store them in a dynamic byte array with a bit_length and capacity in bits. +// +// This is a minimal demonstration. Real usage might require more sophisticated +// memory or bit manipulation for performance. +// ----------------------------------------------------------------------------- + +typedef struct { + // The actual buffer holding the bits (in multiples of 8 bits). + uint8_t *data; + + // The total number of bits currently in use (the "length" of the blob). + size_t bit_length; + + // The total capacity in bits that 'data' can currently hold without realloc. + size_t bit_capacity; + + // 0 = antestone (mutable) + // 1 = stone (immutable) + int is_stone; +} JSBlobData; + +// Forward declaration of class ID and methods +static JSClassID js_blob_class_id; + +// Helper to ensure capacity for writing +// new_bits is additional bits to be appended +static int js_blob_ensure_capacity(JSContext *ctx, JSBlobData *bd, size_t new_bits) { + size_t need_bits = bd->bit_length + new_bits; + if (need_bits <= bd->bit_capacity) return 0; + + // Increase capacity (in multiples of bytes). + // We can pick a growth strategy. For demonstration, double it: + size_t new_capacity = bd->bit_capacity == 0 ? 64 : bd->bit_capacity * 2; + while (new_capacity < need_bits) new_capacity *= 2; + + // Round up new_capacity to a multiple of 8 bits + if (new_capacity % 8) { + new_capacity += 8 - (new_capacity % 8); + } + + size_t new_size_bytes = new_capacity / 8; + uint8_t *new_ptr = realloc(bd->data, new_size_bytes); + if (!new_ptr) { + return -1; // out of memory + } + // zero-fill the new area (only beyond the old capacity) + size_t old_size_bytes = bd->bit_capacity / 8; + if (new_size_bytes > old_size_bytes) { + memset(new_ptr + old_size_bytes, 0, new_size_bytes - old_size_bytes); + } + bd->data = new_ptr; + bd->bit_capacity = new_capacity; + return 0; +} + +// Finalizer for JSBlobData +static void js_blob_finalizer(JSRuntime *rt, JSValue val) { + JSBlobData *bd = JS_GetOpaque(val, js_blob_class_id); + if (bd) { + free(bd->data); + bd->data = NULL; + bd->bit_length = 0; + bd->bit_capacity = 0; + bd->is_stone = 0; + free(bd); + } +} + +// Mark function: not used here, as we have no child JS objects in JSBlobData +static void js_blob_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func) { + // No child JS references to mark +} + +// A helper to create a new JSBlobData object, returning a JSValue wrapping it. +static JSValue js_blob_wrap(JSContext *ctx, JSBlobData *bd) { + JSValue obj = JS_NewObjectClass(ctx, js_blob_class_id); + if (JS_IsException(obj)) { + free(bd->data); + free(bd); + return obj; + } + JS_SetOpaque(obj, bd); + return obj; +} + +// ----------------------------------------------------------------------------- +// Helpers for reading/writing bits +// ----------------------------------------------------------------------------- + +// Write one bit (0 or 1) at the end of the blob +static int js_blob_write_bit_internal(JSContext *ctx, JSBlobData *bd, int bit_val) { + if (bd->is_stone) { + // Trying to write to an immutable blob -> throw + return -1; + } + if (js_blob_ensure_capacity(ctx, bd, 1) < 0) { + return -1; + } + // index in bits + size_t bit_index = bd->bit_length; + size_t byte_index = bit_index >> 3; + size_t offset_in_byte = bit_index & 7; + + // set or clear bit + if (bit_val) + bd->data[byte_index] |= (1 << offset_in_byte); + else + bd->data[byte_index] &= ~(1 << offset_in_byte); + + bd->bit_length++; + return 0; +} + +// Read one bit from a stone blob at position 'pos' +static int js_blob_read_bit_internal(JSBlobData *bd, size_t pos, int *out_bit) { + if (!bd->is_stone) { + // It's not stone -> reading might be out of the specification + // but we can allow or return error. Here we just return error. + return -1; + } + if (pos >= bd->bit_length) { + return -1; // out of range + } + size_t byte_index = pos >> 3; + size_t offset_in_byte = pos & 7; + *out_bit = (bd->data[byte_index] & (1 << offset_in_byte)) ? 1 : 0; + return 0; +} + +// Turn a blob into the "stone" state. This discards any extra capacity. +static void js_blob_make_stone(JSBlobData *bd) { + bd->is_stone = 1; + // Optionally shrink the buffer to exactly bit_length in size + if (bd->bit_capacity > bd->bit_length) { + size_t size_in_bytes = (bd->bit_length + 7) >> 3; // round up to full bytes + uint8_t *new_ptr = NULL; + if (size_in_bytes) { + new_ptr = realloc(bd->data, size_in_bytes); + if (new_ptr) { + bd->data = new_ptr; + } + } else { + // zero length + free(bd->data); + bd->data = NULL; + } + bd->bit_capacity = bd->bit_length; // capacity in bits now matches length + } +} + +// ----------------------------------------------------------------------------- +// JS Functions (blob.make, blob.write_bit, blob.read_logical, etc.) +// ----------------------------------------------------------------------------- + +// blob.make(...) +static JSValue js_blob_make(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + // We'll implement a few typical signatures: + // blob.make() + // blob.make(capacity) + // blob.make(length, logical_value) + // blob.make(blob, from, to) (makes a copy) + // + // This is a simplified approach. The spec mentions random, partial copy, etc. + // We'll handle just these forms enough to demonstrate the concept. + + JSBlobData *bd = calloc(1, sizeof(*bd)); + if (!bd) return JS_ThrowOutOfMemory(ctx); + + // default + bd->data = NULL; + bd->bit_length = 0; + bd->bit_capacity = 0; + bd->is_stone = 0; // initially antestone + + // blob.make() + if (argc == 0) { + // empty antestone blob + } + // blob.make(capacity) + else if (argc == 1 && JS_IsNumber(argv[0])) { + int64_t capacity_bits; + if (JS_ToInt64(ctx, &capacity_bits, argv[0]) < 0) { + free(bd); + return JS_EXCEPTION; + } + if (capacity_bits < 0) capacity_bits = 0; + bd->bit_capacity = (size_t)capacity_bits; + if (bd->bit_capacity % 8) { + bd->bit_capacity += 8 - (bd->bit_capacity % 8); + } + if (bd->bit_capacity) { + size_t bytes = bd->bit_capacity / 8; + bd->data = calloc(bytes, 1); + if (!bd->data) { + free(bd); + return JS_ThrowOutOfMemory(ctx); + } + } + } + // blob.make(length, logical) + else if (argc == 2 && JS_IsNumber(argv[0]) && JS_IsBool(argv[1])) { + int64_t length_bits; + if (JS_ToInt64(ctx, &length_bits, argv[0]) < 0) { + free(bd); + return JS_EXCEPTION; + } + if (length_bits < 0) length_bits = 0; + int is_one = JS_ToBool(ctx, argv[1]); + bd->bit_length = (size_t)length_bits; + bd->bit_capacity = bd->bit_length; + if (bd->bit_capacity % 8) { + bd->bit_capacity += 8 - (bd->bit_capacity % 8); + } + size_t bytes = bd->bit_capacity / 8; + if (bytes) { + bd->data = malloc(bytes); + if (!bd->data) { + free(bd); + return JS_ThrowOutOfMemory(ctx); + } + memset(bd->data, is_one ? 0xff : 0x00, bytes); + // if length_bits isn't a multiple of 8, we need to clear the unused bits + size_t used_bits_in_last_byte = (size_t)length_bits & 7; + if (used_bits_in_last_byte && is_one) { + // clear top bits in the last byte + uint8_t mask = (1 << used_bits_in_last_byte) - 1; + bd->data[bytes - 1] &= mask; + } + } + } + // blob.make(blob, from, to) + else if (argc >= 1 && JS_IsObject(argv[0])) { + // we try copying from another blob if it's of the same class + JSBlobData *src = JS_GetOpaque(argv[0], js_blob_class_id); + if (!src) { + free(bd); + return JS_ThrowTypeError(ctx, "blob.make: argument 1 not a blob"); + } + int64_t from = 0, to = (int64_t)src->bit_length; + if (argc >= 2 && JS_IsNumber(argv[1])) { + JS_ToInt64(ctx, &from, argv[1]); + if (from < 0) from = 0; + } + if (argc >= 3 && JS_IsNumber(argv[2])) { + JS_ToInt64(ctx, &to, argv[2]); + if (to < from) to = from; + if (to > (int64_t)src->bit_length) to = (int64_t)src->bit_length; + } + size_t copy_len = (size_t)(to - from); + bd->bit_length = copy_len; + bd->bit_capacity = copy_len; + if (bd->bit_capacity % 8) { + bd->bit_capacity += 8 - (bd->bit_capacity % 8); + } + size_t bytes = bd->bit_capacity / 8; + if (bytes) { + bd->data = calloc(bytes, 1); + if (!bd->data) { + free(bd); + return JS_ThrowOutOfMemory(ctx); + } + } + // Now copy the bits. + // For simplicity, let's do a naive bit copy one by one: + for (size_t i = 0; i < copy_len; i++) { + size_t src_bit_index = from + i; + size_t src_byte = src_bit_index >> 3; + size_t src_off = src_bit_index & 7; + int bit_val = (src->data[src_byte] >> src_off) & 1; + size_t dst_byte = i >> 3; + size_t dst_off = i & 7; + if (bit_val) { + bd->data[dst_byte] |= (1 << dst_off); + } else { + bd->data[dst_byte] &= ~(1 << dst_off); + } + } + } + // else fail + else { + free(bd); + return JS_ThrowTypeError(ctx, "blob.make: invalid arguments"); + } + + return js_blob_wrap(ctx, bd); +} + +// blob.write_bit(blob, logical) +static JSValue js_blob_write_bit(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc < 2) { + return JS_ThrowTypeError(ctx, "blob.write_bit(blob, logical) requires 2 arguments"); + } + JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id); + if (!bd) { + return JS_ThrowTypeError(ctx, "blob.write_bit: argument 1 not a blob"); + } + int bit_val = JS_ToBool(ctx, argv[1]); // interpret any truthy as 1, else 0 + if (js_blob_write_bit_internal(ctx, bd, bit_val) < 0) { + return JS_ThrowTypeError(ctx, "blob.write_bit: cannot write (maybe stone or OOM)"); + } + return JS_UNDEFINED; +} + +// blob.read_logical(blob, from) +static JSValue js_blob_read_logical(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc < 2) { + return JS_ThrowTypeError(ctx, "blob.read_logical(blob, from) requires 2 arguments"); + } + JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id); + if (!bd) { + return JS_ThrowTypeError(ctx, "blob.read_logical: argument 1 not a blob"); + } + int64_t pos; + if (JS_ToInt64(ctx, &pos, argv[1]) < 0) { + return JS_EXCEPTION; + } + if (pos < 0) { + return JS_NULL; // out of range + } + int bit_val; + if (js_blob_read_bit_internal(bd, (size_t)pos, &bit_val) < 0) { + return JS_NULL; // error or out of range + } + return JS_NewBool(ctx, bit_val); +} + +// blob.stone(blob) +static JSValue js_blob_stone(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc < 1) { + return JS_ThrowTypeError(ctx, "blob.stone(blob) requires 1 argument"); + } + JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id); + if (!bd) { + return JS_ThrowTypeError(ctx, "blob.stone: argument not a blob"); + } + if (!bd->is_stone) { + js_blob_make_stone(bd); + } + return JS_UNDEFINED; +} + +// blob.length(blob) +// Return number of bits in the blob +static JSValue js_blob_length(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc < 1) { + return JS_ThrowTypeError(ctx, "blob.length(blob) requires 1 argument"); + } + JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id); + if (!bd) { + return JS_ThrowTypeError(ctx, "blob.length: argument not a blob"); + } + return JS_NewInt64(ctx, bd->bit_length); +} + +// blob.blob?(value) +// Return true if the value is a blob object +static JSValue js_blob_is_blob(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + if (argc < 1) return JS_FALSE; + JSBlobData *bd = JS_GetOpaque(argv[0], js_blob_class_id); + return JS_NewBool(ctx, bd != NULL); +} + +// ----------------------------------------------------------------------------- +// Exports list +// ----------------------------------------------------------------------------- + +static const JSCFunctionListEntry js_blob_funcs[] = { + // The "make" function. + JS_CFUNC_DEF("make", 3, js_blob_make), + // Some example read/write routines + JS_CFUNC_DEF("write_bit", 2, js_blob_write_bit), + JS_CFUNC_DEF("read_logical", 2, js_blob_read_logical), + // Convert blob from antestone -> stone + JS_CFUNC_DEF("stone", 1, js_blob_stone), + // Return the length in bits + JS_CFUNC_DEF("length", 1, js_blob_length), + // Check if a value is a blob + JS_CFUNC_DEF("isblob", 1, js_blob_is_blob), +}; + +// ----------------------------------------------------------------------------- +// Class definition for the 'blob' objects +// ----------------------------------------------------------------------------- + +static JSClassDef js_blob_class = { + "BlobClass", + .finalizer = js_blob_finalizer, + .gc_mark = js_blob_mark, +}; + +// Module init function +static int js_blob_init(JSContext *ctx, JSModuleDef *m) { + // Register the class if not already done + if (JS_IsRegisteredClass(ctx, js_blob_class_id) == 0) { + JS_NewClassID(&js_blob_class_id); + JS_NewClass(JS_GetRuntime(ctx), js_blob_class_id, &js_blob_class); + } + + // Create a prototype object + JSValue proto = JS_NewObject(ctx); + JS_SetClassProto(ctx, js_blob_class_id, proto); + + // Export our functions as named exports + JS_SetModuleExportList(ctx, m, js_blob_funcs, sizeof(js_blob_funcs) / sizeof(js_blob_funcs[0])); + return 0; +} + +// The module entry point +#ifdef JS_SHARED_LIBRARY +#define JS_INIT_MODULE js_init_module +#else +#define JS_INIT_MODULE js_init_module_blob +#endif + +JSModuleDef *JS_INIT_MODULE(JSContext *ctx, const char *module_name) { + JSModuleDef *m = JS_NewCModule(ctx, module_name, js_blob_init); + if (!m) return NULL; + JS_AddModuleExportList(ctx, m, js_blob_funcs, sizeof(js_blob_funcs) / sizeof(js_blob_funcs[0])); + return m; +} + +// ----------------------------------------------------------------------------- +// js_blob_use(ctx) for easy embedding: returns an object with the blob functions +// ----------------------------------------------------------------------------- + +JSValue js_blob_use(JSContext *ctx) { + // Ensure class is registered + if (JS_IsRegisteredClass(ctx, js_blob_class_id) == 0) { + JS_NewClassID(&js_blob_class_id); + JS_NewClass(JS_GetRuntime(ctx), js_blob_class_id, &js_blob_class); + + // Create a prototype object + JSValue proto = JS_NewObject(ctx); + JS_SetClassProto(ctx, js_blob_class_id, proto); + } + + // Create a plain object (the "exports") and add the funcs + JSValue obj = JS_NewObject(ctx); + JS_SetPropertyFunctionList(ctx, obj, js_blob_funcs, + sizeof(js_blob_funcs) / sizeof(js_blob_funcs[0])); + return obj; +} diff --git a/source/qjs_blob.h b/source/qjs_blob.h new file mode 100644 index 00000000..7af464c3 --- /dev/null +++ b/source/qjs_blob.h @@ -0,0 +1,8 @@ +#ifndef QJS_BLOB_H +#define QJS_BLOB_H + +#include + +JSValue js_blob_use(JSContext *ctx); + +#endif diff --git a/source/qjs_crypto.c b/source/qjs_crypto.c new file mode 100644 index 00000000..f9cf79f1 --- /dev/null +++ b/source/qjs_crypto.c @@ -0,0 +1,227 @@ +#include "qjs_crypto.h" +#include "quickjs.h" +#include +#include +#include + +#include "monocypher.h" + +// randombytes.c - Minimal cross-platform CSPRNG shim (single file) +/* + Usage: + #include "randombytes.c" + + int main() { + uint8_t buffer[32]; + if (randombytes(buffer, sizeof(buffer)) != 0) { + // handle error + } + // buffer now has 32 cryptographically secure random bytes + } +*/ + +#include +#include + +#if defined(_WIN32) +// ------- Windows: use BCryptGenRandom ------- +#include +#include + +int randombytes(void *buf, size_t n) { + NTSTATUS status = BCryptGenRandom(NULL, (PUCHAR)buf, (ULONG)n, BCRYPT_USE_SYSTEM_PREFERRED_RNG); + return (status == 0) ? 0 : -1; +} + +#elif defined(__linux__) +// ------- Linux: try getrandom, fall back to /dev/urandom ------- +#include +#include +#include +#include + +// If we have a new enough libc and kernel, getrandom is available. +// Otherwise, we’ll do a /dev/urandom fallback. +#include + +static int randombytes_fallback(void *buf, size_t n) { + int fd = open("/dev/urandom", O_RDONLY); + if (fd < 0) return -1; + ssize_t r = read(fd, buf, n); + close(fd); + return (r == (ssize_t)n) ? 0 : -1; +} + +int randombytes(void *buf, size_t n) { +#ifdef SYS_getrandom + // Try getrandom(2) if available + ssize_t ret = syscall(SYS_getrandom, buf, n, 0); + if (ret < 0) { + // If getrandom is not supported or fails, fall back + if (errno == ENOSYS) { + return randombytes_fallback(buf, n); + } + return -1; + } + return (ret == (ssize_t)n) ? 0 : -1; +#else + // getrandom not available, just fallback + return randombytes_fallback(buf, n); +#endif +} + +#else +// ------- Other Unix: read from /dev/urandom ------- +#include +#include + +int randombytes(void *buf, size_t n) { + int fd = open("/dev/urandom", O_RDONLY); + if (fd < 0) return -1; + ssize_t r = read(fd, buf, n); + close(fd); + return (r == (ssize_t)n) ? 0 : -1; +} +#endif + +static inline void to_hex(const uint8_t *in, size_t in_len, char *out) +{ + static const char hexchars[] = "0123456789abcdef"; + for (size_t i = 0; i < in_len; i++) { + out[2*i ] = hexchars[(in[i] >> 4) & 0x0F]; + out[2*i + 1] = hexchars[ in[i] & 0x0F]; + } + out[2 * in_len] = '\0'; // null-terminate +} + +static inline int nibble_from_char(char c, uint8_t *nibble) +{ + if (c >= '0' && c <= '9') { *nibble = (uint8_t)(c - '0'); return 0; } + if (c >= 'a' && c <= 'f') { *nibble = (uint8_t)(c - 'a' + 10); return 0; } + if (c >= 'A' && c <= 'F') { *nibble = (uint8_t)(c - 'A' + 10); return 0; } + return -1; // invalid char +} + +static inline int from_hex(const char *hex, uint8_t *out, size_t out_len) +{ + for (size_t i = 0; i < out_len; i++) { + uint8_t hi, lo; + if (nibble_from_char(hex[2*i], &hi) < 0) return -1; + if (nibble_from_char(hex[2*i + 1], &lo) < 0) return -1; + out[i] = (uint8_t)((hi << 4) | lo); + } + return 0; +} + +// Convert a JSValue containing a 64-character hex string into a 32-byte array. +static inline void js2crypto(JSContext *js, JSValue v, uint8_t *crypto) +{ + size_t hex_len; + const char *hex_str = JS_ToCStringLen(js, &hex_len, v); + if (!hex_str) + return; + + if (hex_len != 64) { + JS_FreeCString(js, hex_str); + JS_ThrowTypeError(js, "js2crypto: expected 64-hex-char string"); + return; + } + + if (from_hex(hex_str, crypto, 32) < 0) { + JS_FreeCString(js, hex_str); + JS_ThrowTypeError(js, "js2crypto: invalid hex encoding"); + return; + } + + JS_FreeCString(js, hex_str); +} + +static inline JSValue crypto2js(JSContext *js, const uint8_t *crypto) +{ + char hex[65]; // 32*2 + 1 for null terminator + to_hex(crypto, 32, hex); + return JS_NewString(js, hex); +} + +JSValue js_crypto_keypair(JSContext *js, JSValue self, int argc, JSValue *argv) { + JSValue ret = JS_NewObject(js); + + uint8_t public[32]; + uint8_t private[32]; + + randombytes(private,32); + + private[0] &= 248; + private[31] &= 127; + private[31] |= 64; + + crypto_x25519_public_key(public,private); + + JS_SetPropertyStr(js, ret, "public", crypto2js(js, public)); + JS_SetPropertyStr(js, ret, "private", crypto2js(js,private)); + return ret; +} + +JSValue js_crypto_shared(JSContext *js, JSValue self, int argc, JSValue *argv) +{ + if (argc < 1 || !JS_IsObject(argv[0])) { + return JS_ThrowTypeError(js, "crypto.shared: expected an object argument"); + } + + JSValue obj = argv[0]; + + JSValue val_pub = JS_GetPropertyStr(js, obj, "public"); + if (JS_IsException(val_pub)) { + JS_FreeValue(js, val_pub); + return JS_EXCEPTION; + } + + JSValue val_priv = JS_GetPropertyStr(js, obj, "private"); + if (JS_IsException(val_priv)) { + JS_FreeValue(js, val_pub); + JS_FreeValue(js, val_priv); + return JS_EXCEPTION; + } + + uint8_t pub[32], priv[32]; + js2crypto(js, val_pub, pub); + js2crypto(js, val_priv, priv); + + JS_FreeValue(js, val_pub); + JS_FreeValue(js, val_priv); + + uint8_t shared[32]; + crypto_x25519(shared, priv, pub); + + return crypto2js(js, shared); +} + +JSValue js_crypto_random(JSContext *js, JSValue self, int argc, JSValue *argv) +{ + // 1) Pull 64 bits of cryptographically secure randomness + uint64_t r; + if (randombytes(&r, sizeof(r)) != 0) { + // If something fails (extremely rare), throw an error + return JS_ThrowInternalError(js, "crypto.random: unable to get random bytes"); + } + + // 2) Convert r to a double in the range [0,1). + // We divide by (UINT64_MAX + 1.0) to ensure we never produce exactly 1.0. + double val = (double)r / ((double)UINT64_MAX + 1.0); + + // 3) Return that as a JavaScript number + return JS_NewFloat64(js, val); +} + +static const JSCFunctionListEntry js_crypto_funcs[] = { + JS_CFUNC_DEF("keypair", 0, js_crypto_keypair), + JS_CFUNC_DEF("shared", 1, js_crypto_shared), + JS_CFUNC_DEF("random", 0, js_crypto_random), +}; + +JSValue js_crypto_use(JSContext *js) +{ + JSValue obj = JS_NewObject(js); + JS_SetPropertyFunctionList(js, obj, js_crypto_funcs, sizeof(js_crypto_funcs)/sizeof(js_crypto_funcs[0])); + return obj; +} diff --git a/source/qjs_crypto.h b/source/qjs_crypto.h new file mode 100644 index 00000000..3a2d3839 --- /dev/null +++ b/source/qjs_crypto.h @@ -0,0 +1,8 @@ +#ifndef QJS_CRYPTO_H +#define QJS_CRYPTO_H + +#include "quickjs.h" + +JSValue js_crypto_use(JSContext *ctx); + +#endif diff --git a/source/qjs_renderer.c b/source/qjs_renderer.c new file mode 100644 index 00000000..d63e6d06 --- /dev/null +++ b/source/qjs_renderer.c @@ -0,0 +1,506 @@ +#include "qjs_renderer.h" +#include "quickjs.h" + +#include +#include "qjs_macros.h" + +#include +#include +#include +#include "HandmadeMath.h" + +QJSCLASS(SDL_Renderer,) + +rect transform_rect(rect in, HMM_Mat3 *t) +{ + HMM_Vec3 bottom_left = (HMM_Vec3){in.x,in.y,1.0}; + HMM_Vec3 transformed_bl = HMM_MulM3V3(*t, bottom_left); + in.x = transformed_bl.x; + in.y = transformed_bl.y; + in.y = in.y - in.h; // should be done for any platform that draws rectangles from top left + return in; +} + +HMM_Vec2 transform_point(SDL_Renderer *ren, HMM_Vec2 in, HMM_Mat3 *t) +{ + rect logical; + SDL_GetRenderLogicalPresentationRect(ren, &logical); + in.y *= -1; + in.y += logical.h; + in.x -= t->Columns[2].x; + in.y -= t->Columns[2].y; + return in; +} + +JSC_CCALL(SDL_Renderer_clear, + SDL_Renderer *renderer = js2SDL_Renderer(js,self); + SDL_RenderClear(renderer); +) + +JSC_CCALL(SDL_Renderer_present, + SDL_Renderer *ren = js2SDL_Renderer(js,self); + SDL_RenderPresent(ren); +) + +JSC_CCALL(SDL_Renderer_draw_color, + SDL_Renderer *renderer = js2SDL_Renderer(js,self); + colorf color = js2color(js,argv[0]); + SDL_SetRenderDrawColorFloat(renderer, color.r,color.g,color.b,color.a); +) + +JSC_CCALL(SDL_Renderer_rect, + SDL_Renderer *r = js2SDL_Renderer(js,self); + if (!JS_IsUndefined(argv[1])) { + colorf color = js2color(js,argv[1]); + SDL_SetRenderDrawColorFloat(r, color.r, color.g, color.b, color.a); + } + + if (JS_IsArray(js,argv[0])) { + int len = js_arrlen(js,argv[0]); + rect rects[len]; + for (int i = 0; i < len; i++) { + JSValue val = JS_GetPropertyUint32(js,argv[0],i); + rects[i] = transform_rect(js2rect(js,val), &cam_mat); + JS_FreeValue(js,val); + } + SDL_RenderRects(r,rects,len); + return JS_UNDEFINED; + } + + rect rect = js2rect(js,argv[0]); + + rect = transform_rect(rect, &cam_mat); + + SDL_RenderRect(r, &rect); +) + +JSC_CCALL(renderer_load_texture, + SDL_Renderer *r = js2SDL_Renderer(js,self); + SDL_Surface *surf = js2SDL_Surface(js,argv[0]); + if (!surf) return JS_ThrowReferenceError(js, "Surface was not a surface."); + SDL_Texture *tex = SDL_CreateTextureFromSurface(r,surf); + if (!tex) return JS_ThrowReferenceError(js, "Could not create texture from surface: %s", SDL_GetError()); + ret = SDL_Texture2js(js,tex); + JS_SetProperty(js,ret,width, number2js(js,tex->w)); + JS_SetProperty(js,ret,height, number2js(js,tex->h)); +) + +JSC_CCALL(SDL_Renderer_fillrect, + SDL_Renderer *r = js2SDL_Renderer(js,self); + if (!JS_IsUndefined(argv[1])) { + colorf color = js2color(js,argv[1]); + SDL_SetRenderDrawColorFloat(r, color.r, color.g, color.b, color.a); + } + + if (JS_IsArray(js,argv[0])) { + int len = js_arrlen(js,argv[0]); + rect rects[len]; + for (int i = 0; i < len; i++) { + JSValue val = JS_GetPropertyUint32(js,argv[0],i); + rects[i] = js2rect(js,val); + JS_FreeValue(js,val); + } + if (!SDL_RenderFillRects(r,rects,len)) + return JS_ThrowReferenceError(js, "Could not render rectangle: %s", SDL_GetError()); + } + rect rect = transform_rect(js2rect(js,argv[0]),&cam_mat); + + if (!SDL_RenderFillRect(r, &rect)) + return JS_ThrowReferenceError(js, "Could not render rectangle: %s", SDL_GetError()); +) + +JSC_CCALL(renderer_texture, + SDL_Renderer *renderer = js2SDL_Renderer(js,self); + SDL_Texture *tex = js2SDL_Texture(js,argv[0]); + rect dst = transform_rect(js2rect(js,argv[1]), &cam_mat); + + if (!JS_IsUndefined(argv[3])) { + colorf color = js2color(js,argv[3]); + SDL_SetTextureColorModFloat(tex, color.r, color.g, color.b); + SDL_SetTextureAlphaModFloat(tex,color.a); + } + if (JS_IsUndefined(argv[2])) + SDL_RenderTexture(renderer,tex,NULL,&dst); + else { + + rect src = js2rect(js,argv[2]); + + SDL_RenderTextureRotated(renderer, tex, &src, &dst, 0, NULL, SDL_FLIP_NONE); + } +) + +JSC_CCALL(renderer_tile, + SDL_Renderer *renderer = js2SDL_Renderer(js,self); + if (!renderer) return JS_ThrowTypeError(js,"self was not a renderer"); + SDL_Texture *tex = js2SDL_Texture(js,argv[0]); + if (!tex) return JS_ThrowTypeError(js,"first argument was not a texture"); + rect dst = js2rect(js,argv[1]); + if (!dst.w) dst.w = tex->w; + if (!dst.h) dst.h = tex->h; + float scale = js2number(js,argv[3]); + if (!scale) scale = 1; + if (JS_IsUndefined(argv[2])) + SDL_RenderTextureTiled(renderer,tex,NULL,scale, &dst); + else { + rect src = js2rect(js,argv[2]); + SDL_RenderTextureTiled(renderer,tex,&src,scale, &dst); + } +) + +JSC_CCALL(renderer_slice9, + SDL_Renderer *renderer = js2SDL_Renderer(js,self); + SDL_Texture *tex = js2SDL_Texture(js,argv[0]); + lrtb bounds = js2lrtb(js,argv[2]); + rect src, dst; + src = transform_rect(js2rect(js,argv[3]),&cam_mat); + dst = transform_rect(js2rect(js,argv[1]), &cam_mat); + + SDL_RenderTexture9Grid(renderer, tex, + JS_IsUndefined(argv[3]) ? NULL : &src, + bounds.l, bounds.r, bounds.t, bounds.b, 0.0, + JS_IsUndefined(argv[1]) ? NULL : &dst); +) + +JSC_CCALL(renderer_get_image, + SDL_Renderer *r = js2SDL_Renderer(js,self); + SDL_Surface *surf = NULL; + if (!JS_IsUndefined(argv[0])) { + rect rect = js2rect(js,argv[0]); + surf = SDL_RenderReadPixels(r,&rect); + } else + surf = SDL_RenderReadPixels(r,NULL); + if (!surf) return JS_ThrowReferenceError(js, "could not make surface from renderer"); + return SDL_Surface2js(js,surf); +) + +JSC_SCALL(renderer_fasttext, + SDL_Renderer *r = js2SDL_Renderer(js,self); + if (!JS_IsUndefined(argv[2])) { + colorf color = js2color(js,argv[2]); + SDL_SetRenderDrawColorFloat(r, color.r, color.g, color.b, color.a); + } + HMM_Vec2 pos = js2vec2(js,argv[1]); + pos.y += 8; + HMM_Vec2 tpos = HMM_MulM3V3(cam_mat, (HMM_Vec3){pos.x,pos.y,1}).xy; + SDL_RenderDebugText(r, tpos.x, tpos.y, str); +) + +JSC_CCALL(renderer_line, + SDL_Renderer *r = js2SDL_Renderer(js,self); + if (!JS_IsUndefined(argv[1])) { + colorf color = js2color(js,argv[1]); + SDL_SetRenderDrawColorFloat(r, color.r, color.g, color.b, color.a); + } + + if (JS_IsArray(js,argv[0])) { + int len = js_arrlen(js,argv[0]); + HMM_Vec2 points[len]; + assert(sizeof(HMM_Vec2) == sizeof(SDL_FPoint)); + for (int i = 0; i < len; i++) { + JSValue val = JS_GetPropertyUint32(js,argv[0],i); + points[i] = js2vec2(js,val); + JS_FreeValue(js,val); + } + SDL_RenderLines(r,points,len); + } +) + +JSC_CCALL(renderer_point, + SDL_Renderer *r = js2SDL_Renderer(js,self); + if (!JS_IsUndefined(argv[1])) { + colorf color = js2color(js,argv[1]); + SDL_SetRenderDrawColorFloat(r, color.r, color.g, color.b, color.a); + } + + if (JS_IsArray(js,argv[0])) { + int len = js_arrlen(js,argv[0]); + HMM_Vec2 points[len]; + assert(sizeof(HMM_Vec2) ==sizeof(SDL_FPoint)); + for (int i = 0; i < len; i++) { + JSValue val = JS_GetPropertyUint32(js,argv[0],i); + points[i] = js2vec2(js,val); + JS_FreeValue(js,val); + } + SDL_RenderPoints(r, points, len); + return JS_UNDEFINED; + } + + HMM_Vec2 point = transform_point(r, js2vec2(js,argv[0]), &cam_mat); + SDL_RenderPoint(r,point.x,point.y); +) + +// Function to translate a list of 2D points +void Translate2DPoints(HMM_Vec2 *points, int count, HMM_Vec3 position, HMM_Quat rotation, HMM_Vec3 scale) { + // Precompute the 2D rotation matrix from the quaternion + float xx = rotation.x * rotation.x; + float yy = rotation.y * rotation.y; + float zz = rotation.z * rotation.z; + float xy = rotation.x * rotation.y; + float zw = rotation.z * rotation.w; + + // Extract 2D affine rotation and scaling + float m00 = (1.0f - 2.0f * (yy + zz)) * scale.x; // Row 1, Column 1 + float m01 = (2.0f * (xy + zw)) * scale.y; // Row 1, Column 2 + float m10 = (2.0f * (xy - zw)) * scale.x; // Row 2, Column 1 + float m11 = (1.0f - 2.0f * (xx + zz)) * scale.y; // Row 2, Column 2 + + // Translation components (ignore the z position) + float tx = position.x; + float ty = position.y; + + // Transform each point + for (int i = 0; i < count; ++i) { + HMM_Vec2 p = points[i]; + points[i].x = m00 * p.x + m01 * p.y + tx; + points[i].y = m10 * p.x + m11 * p.y + ty; + } +} + +// Should take a single struct with pos, color, uv, and indices arrays +JSC_CCALL(renderer_geometry, + SDL_Renderer *r = js2SDL_Renderer(js,self); + JSValue pos = JS_GetPropertyStr(js,argv[1], "pos"); + JSValue color = JS_GetPropertyStr(js,argv[1], "color"); + JSValue uv = JS_GetPropertyStr(js,argv[1], "uv"); + JSValue indices = JS_GetPropertyStr(js,argv[1], "indices"); + int vertices = js_getnum_str(js, argv[1], "vertices"); + int count = js_getnum_str(js, argv[1], "count"); + + size_t pos_stride, indices_stride, uv_stride, color_stride; + void *posdata = get_gpu_buffer(js,pos, &pos_stride, NULL); + void *idxdata = get_gpu_buffer(js,indices, &indices_stride, NULL); + void *uvdata = get_gpu_buffer(js,uv, &uv_stride, NULL); + void *colordata = get_gpu_buffer(js,color,&color_stride, NULL); + + SDL_Texture *tex = js2SDL_Texture(js,argv[0]); + + HMM_Vec2 *trans_pos = malloc(vertices*sizeof(HMM_Vec2)); + memcpy(trans_pos,posdata, sizeof(HMM_Vec2)*vertices); + + for (int i = 0; i < vertices; i++) + trans_pos[i] = HMM_MulM3V3(cam_mat, (HMM_Vec3){trans_pos[i].x, trans_pos[i].y, 1}).xy; + + if (!SDL_RenderGeometryRaw(r, tex, trans_pos, pos_stride,colordata,color_stride,uvdata, uv_stride, vertices, idxdata, count, indices_stride)) + ret = JS_ThrowReferenceError(js, "Error rendering geometry: %s",SDL_GetError()); + + free(trans_pos); + + JS_FreeValue(js,pos); + JS_FreeValue(js,color); + JS_FreeValue(js,uv); + JS_FreeValue(js,indices); +) + +JSC_CCALL(renderer_logical_size, + SDL_Renderer *r = js2SDL_Renderer(js,self); + HMM_Vec2 v = js2vec2(js,argv[0]); + SDL_SetRenderLogicalPresentation(r,v.x,v.y,SDL_LOGICAL_PRESENTATION_INTEGER_SCALE); +) + +JSC_CCALL(renderer_viewport, + SDL_Renderer *r = js2SDL_Renderer(js,self); + if (JS_IsUndefined(argv[0])) + SDL_SetRenderViewport(r,NULL); + else { + rect view = js2rect(js,argv[0]); + SDL_SetRenderViewport(r,&view); + } +) + +JSC_CCALL(renderer_get_viewport, + SDL_Renderer *r = js2SDL_Renderer(js,self); + SDL_Rect vp; + SDL_GetRenderViewport(r, &vp); + rect re; + re.x = vp.x; + re.y = vp.y; + re.h = vp.h; + re.w = vp.w; + return rect2js(js,re); +) + +JSC_CCALL(renderer_clip, + SDL_Renderer *r = js2SDL_Renderer(js,self); + if (JS_IsUndefined(argv[0])) + SDL_SetRenderClipRect(r,NULL); + else { + rect view = js2rect(js,argv[0]); + SDL_SetRenderClipRect(r,&view); + } +) + +JSC_CCALL(renderer_scale, + SDL_Renderer *r = js2SDL_Renderer(js,self); + HMM_Vec2 v = js2vec2(js,argv[0]); + SDL_SetRenderScale(r, v.x, v.y); +) + +JSC_CCALL(renderer_vsync, + SDL_Renderer *r = js2SDL_Renderer(js,self); + SDL_SetRenderVSync(r,js2number(js,argv[0])); +) + +// This returns the coordinates inside the +JSC_CCALL(renderer_coords, + SDL_Renderer *r = js2SDL_Renderer(js,self); + HMM_Vec2 pos, coord; + pos = js2vec2(js,argv[0]); + SDL_RenderCoordinatesFromWindow(r,pos.x,pos.y, &coord.x, &coord.y); + return vec22js(js,coord); +) + +JSC_CCALL(renderer_camera, + int centered = JS_ToBool(js,argv[1]); + SDL_Renderer *ren = js2SDL_Renderer(js,self); + SDL_Rect vp; + SDL_GetRenderViewport(ren, &vp); + HMM_Mat3 proj; + proj.Columns[0] = (HMM_Vec3){1,0,0}; + proj.Columns[1] = (HMM_Vec3){0,-1,0}; + if (centered) + proj.Columns[2] = (HMM_Vec3){vp.w/2.0,vp.h/2.0,1}; + else + proj.Columns[2] = (HMM_Vec3){0,vp.h,1}; + + transform *tra = js2transform(js,argv[0]); + HMM_Mat3 view; + view.Columns[0] = (HMM_Vec3){1,0,0}; + view.Columns[1] = (HMM_Vec3){0,1,0}; + view.Columns[2] = (HMM_Vec3){-tra->pos.x, -tra->pos.y,1}; + cam_mat = HMM_MulM3(proj,view); +) + +JSC_CCALL(renderer_screen2world, + HMM_Mat3 inv = HMM_InvGeneralM3(cam_mat); + HMM_Vec3 pos = js2vec3(js,argv[0]); + return vec22js(js, HMM_MulM3V3(inv, pos).xy); +) + +JSC_CCALL(renderer_target, + SDL_Renderer *r = js2SDL_Renderer(js,self); + if (JS_IsUndefined(argv[0])) + SDL_SetRenderTarget(r, NULL); + else { + SDL_Texture *tex = js2SDL_Texture(js,argv[0]); + SDL_SetRenderTarget(r,tex); + } +) + +// Given an array of sprites, make the necessary geometry +// A sprite is expected to have: +// transform: a transform encoding position and rotation. its scale is in pixels - so a scale of 1 means the image will draw only on a single pixel. +// image: a standard prosperon image of a surface, rect, and texture +// color: the color this sprite should be hued by +JSC_CCALL(renderer_make_sprite_mesh, + JSValue sprites = argv[0]; + size_t quads = js_arrlen(js,argv[0]); + size_t verts = quads*4; + size_t count = quads*6; + + HMM_Vec2 *posdata = malloc(sizeof(*posdata)*verts); + HMM_Vec2 *uvdata = malloc(sizeof(*uvdata)*verts); + HMM_Vec4 *colordata = malloc(sizeof(*colordata)*verts); + + for (int i = 0; i < quads; i++) { + JSValue sub = JS_GetPropertyUint32(js,sprites,i); + JSValue jstransform = JS_GetProperty(js,sub,transform); + + JSValue jssrc = JS_GetProperty(js,sub,src); + JSValue jscolor = JS_GetProperty(js,sub,color); + HMM_Vec4 color; + + rect src; + if (JS_IsUndefined(jssrc)) + src = (rect){.x = 0, .y = 0, .w = 1, .h = 1}; + else + src = js2rect(js,jssrc); + + if (JS_IsUndefined(jscolor)) + color = (HMM_Vec4){1,1,1,1}; + else + color = js2vec4(js,jscolor); + + // Calculate the base index for the current quad + size_t base = i * 4; + + // Define the UV coordinates based on the source rectangle + uvdata[base + 0] = (HMM_Vec2){ src.x, src.y + src.h }; + uvdata[base + 1] = (HMM_Vec2){ src.x + src.w, src.y + src.h }; + uvdata[base + 2] = (HMM_Vec2){ src.x, src.y }; + uvdata[base + 3] = (HMM_Vec2){ src.x + src.w, src.y }; + + colordata[base] = color; + colordata[base+1] = color; + colordata[base+2] = color; + colordata[base+3] = color; + + JS_FreeValue(js,jstransform); + JS_FreeValue(js,sub); + JS_FreeValue(js,jscolor); + JS_FreeValue(js,jssrc); + } + + ret = JS_NewObject(js); + JS_SetProperty(js, ret, pos, make_gpu_buffer(js, posdata, sizeof(*posdata) * verts, JS_TYPED_ARRAY_FLOAT32, 2, 0,0)); + JS_SetProperty(js, ret, uv, make_gpu_buffer(js, uvdata, sizeof(*uvdata) * verts, JS_TYPED_ARRAY_FLOAT32, 2, 0,0)); + JS_SetProperty(js, ret, color, make_gpu_buffer(js, colordata, sizeof(*colordata) * verts, JS_TYPED_ARRAY_FLOAT32, 4, 0,0)); + JS_SetProperty(js, ret, indices, make_quad_indices_buffer(js, quads)); + JS_SetProperty(js, ret, vertices, number2js(js, verts)); + JS_SetProperty(js, ret, count, number2js(js, count)); +) + +static const JSCFunctionListEntry js_renderer_funcs[] = { + JS_CFUNC_DEF("clear", 0, js_renderer_clear), + JS_CFUNC_DEF("present", 0, js_renderer_present), + JS_CFUNC_DEF("draw_color", 1, js_renderer_draw_color), + JS_CFUNC_DEF("rect", 2, js_renderer_rect), + JS_CFUNC_DEF("fillrect", 2, js_renderer_fillrect), + JS_CFUNC_DEF("line", 2, js_renderer_line), + JS_CFUNC_DEF("point", 2, js_renderer_point), + JS_CFUNC_DEF("load_texture", 1, js_renderer_load_texture), + JS_CFUNC_DEF("texture", 4, js_renderer_texture), + JS_CFUNC_DEF("slice9", 4, js_renderer_slice9), + JS_CFUNC_DEF("tile", 4, js_renderer_tile), + JS_CFUNC_DEF("get_image", 1, js_renderer_get_image), + JS_CFUNC_DEF("fasttext", 2, js_renderer_fasttext), + JS_CFUNC_DEF("geometry", 2, js_renderer_geometry), + JS_CFUNC_DEF("scale", 1, js_renderer_scale), + JS_CFUNC_DEF("logical_size", 1, js_renderer_logical_size), + JS_CFUNC_DEF("viewport", 1, js_renderer_viewport), + JS_CFUNC_DEF("clip", 1, js_renderer_clip), + JS_CFUNC_DEF("vsync", 1, js_renderer_vsync), + JS_CFUNC_DEF("coords", 1, js_renderer_coords), + JS_CFUNC_DEF("camera", 2, js_renderer_camera), + JS_CFUNC_DEF("get_viewport", 0, js_renderer_get_viewport), + JS_CFUNC_DEF("screen2world", 1, js_renderer_screen2world), + JS_CFUNC_DEF("target", 1, js_renderer_target), + JS_CFUNC_DEF("make_sprite_mesh",2, js_renderer_make_sprite_mesh), +}; + +JSC_CCALL(mod_create, + SDL_Window *win = js2SDL_Window(js,self); + SDL_PropertiesID props = SDL_CreateProperties(); + SDL_SetNumberProperty(props, SDL_PROP_RENDERER_CREATE_PRESENT_VSYNC_NUMBER, 0); + SDL_SetPointerProperty(props, SDL_PROP_RENDERER_CREATE_WINDOW_POINTER, win); + SDL_SetStringProperty(props, SDL_PROP_RENDERER_CREATE_NAME_STRING, str); + SDL_Renderer *r = SDL_CreateRendererWithProperties(props); + SDL_DestroyProperties(props); + if (!r) return JS_ThrowReferenceError(js, "Error creating renderer: %s",SDL_GetError()); + SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND); + return SDL_Renderer2js(js,r); +) + +static const JSCFunctionListEntry js_mod_funcs[] = { + JS_CFUNC_DEF("create", 1, js_mod_create) +}; + +JSValue js_renderer_use(JSContext *ctx) { + // Create an object that will hold all the "renderer" methods + JSValue obj = JS_NewObject(ctx); + + // Add all the above C functions as properties of that object + JS_SetPropertyFunctionList(ctx, obj, + js_renderer_funcs, + sizeof(js_renderer_funcs)/sizeof(JSCFunctionListEntry)); + return obj; +} diff --git a/source/qjs_renderer.h b/source/qjs_renderer.h new file mode 100644 index 00000000..4184f15f --- /dev/null +++ b/source/qjs_renderer.h @@ -0,0 +1,8 @@ +#ifndef QJS_RENDERER_H +#define QJS_RENDERER_H + +#include "quickjs.h" + +JSValue js_renderer_use(JSContext *ctx); + +#endif /* QJS_RENDERER_H */ diff --git a/source/qjs_time.c b/source/qjs_time.c new file mode 100644 index 00000000..6e01e251 --- /dev/null +++ b/source/qjs_time.c @@ -0,0 +1,45 @@ +#include "qjs_time.h" +#include "quickjs.h" +#include // For time() calls, localtime, etc. +#include // For gettimeofday, if needed + +// Example stubs for your time-related calls + +static JSValue js_time_now(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + // time_now + struct timeval tv; + gettimeofday(&tv, NULL); + double now = (double)tv.tv_sec + (tv.tv_usec / 1000000.0); + return JS_NewFloat64(ctx, now); +} + +static JSValue js_time_computer_dst(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + time_t t = time(NULL); + struct tm *lt = localtime(&t); + int is_dst = (lt ? lt->tm_isdst : -1); + return JS_NewBool(ctx, (is_dst > 0)); +} + +static JSValue js_time_computer_zone(JSContext *ctx, JSValueConst this_val, + int argc, JSValueConst *argv) { + time_t t = time(NULL); + time_t local_t = mktime(localtime(&t)); + double diff = difftime(t, local_t); // difference in seconds from local time + return JS_NewFloat64(ctx, diff / 3600.0); +} + +static const JSCFunctionListEntry js_time_funcs[] = { + // name, prop flags, #args, etc. + JS_CFUNC_DEF("now", 0, js_time_now), + JS_CFUNC_DEF("computer_dst", 0, js_time_computer_dst), + JS_CFUNC_DEF("computer_zone", 0, js_time_computer_zone), +}; + +JSValue js_time_use(JSContext *ctx) { + JSValue obj = JS_NewObject(ctx); + JS_SetPropertyFunctionList(ctx, obj, js_time_funcs, + sizeof(js_time_funcs)/sizeof(js_time_funcs[0])); + return obj; +} diff --git a/source/qjs_time.h b/source/qjs_time.h new file mode 100644 index 00000000..475e78df --- /dev/null +++ b/source/qjs_time.h @@ -0,0 +1,8 @@ +#ifndef QJS_TIME_H +#define QJS_TIME_H + +#include "quickjs.h" + +JSValue js_time_use(JSContext *ctx); + +#endif /* QJS_TIME_H */ diff --git a/tests/blob.js b/tests/blob.js new file mode 100644 index 00000000..88bf2d7c --- /dev/null +++ b/tests/blob.js @@ -0,0 +1,321 @@ +// +// test_blob.js +// +// Example test script for qjs_blob.c/h +// +// Run in QuickJS, e.g. +// qjs -m test_blob.js +// + +// Attempt to "use" the blob module as if it was installed or compiled in. +var blob = use('blob'); + +// If you're testing in an environment without a 'use' loader, you might do +// something like importing the compiled C module or linking it differently. + +// (Optional) if you have an 'os' module available for controlling exit codes: +var os = undefined; +try { + os = use('os'); +} catch (e) { + // If there's no 'os' module, ignore +} + +// A small tolerance for floating comparisons if needed +var EPSILON = 1e-12; + +function deepCompare(expected, actual, path = '') { + // Basic triple-equals check + if (expected === actual) { + return { passed: true, messages: [] }; + } + + // Compare booleans + if (typeof expected === 'boolean' && typeof actual === 'boolean') { + return { + passed: false, + messages: [ + `Boolean mismatch at ${path}: expected ${expected}, got ${actual}` + ] + }; + } + + // Compare numbers with tolerance + if (typeof expected === 'number' && typeof actual === 'number') { + if (isNaN(expected) && isNaN(actual)) { + return { passed: true, messages: [] }; + } + const diff = Math.abs(expected - actual); + if (diff <= EPSILON) { + return { passed: true, messages: [] }; + } + return { + passed: false, + messages: [ + `Number mismatch at ${path}: expected ${expected}, got ${actual}`, + `Difference of ${diff} > EPSILON (${EPSILON})` + ] + }; + } + + // Compare arrays + if (Array.isArray(expected) && Array.isArray(actual)) { + if (expected.length !== actual.length) { + return { + passed: false, + messages: [ + `Array length mismatch at ${path}: expected len=${expected.length}, got len=${actual.length}` + ] + }; + } + let messages = []; + for (let i = 0; i < expected.length; i++) { + let r = deepCompare(expected[i], actual[i], `${path}[${i}]`); + if (!r.passed) messages.push(...r.messages); + } + return { + passed: messages.length === 0, + messages + }; + } + + // Compare objects + if ( + typeof expected === 'object' && + expected !== null && + typeof actual === 'object' && + actual !== null + ) { + let expKeys = Object.keys(expected).sort(); + let actKeys = Object.keys(actual).sort(); + if (JSON.stringify(expKeys) !== JSON.stringify(actKeys)) { + return { + passed: false, + messages: [ + `Object keys mismatch at ${path}: expected [${expKeys}], got [${actKeys}]` + ] + }; + } + let messages = []; + for (let k of expKeys) { + let r = deepCompare(expected[k], actual[k], path ? path + '.' + k : k); + if (!r.passed) messages.push(...r.messages); + } + return { passed: messages.length === 0, messages }; + } + + // If none of the above, treat as a mismatch + return { + passed: false, + messages: [ + `Mismatch at ${path}: expected ${JSON.stringify( + expected + )}, got ${JSON.stringify(actual)}` + ] + }; +} + +// Helper to record the results of a single test +function runTest(testName, testFn) { + let passed = true, messages = []; + try { + const result = testFn(); + if (typeof result === 'object' && result !== null) { + passed = result.passed; + messages = result.messages || []; + } else { + // If the testFn returned a boolean or no return, interpret it + passed = !!result; + } + } catch (e) { + passed = false; + messages.push(`Exception thrown: ${e.stack || e.toString()}`); + } + return { testName, passed, messages }; +} + +// --------------------------------------------------------------------------- +// The test suite +// --------------------------------------------------------------------------- +let tests = [ + + // 1) Ensure we can create a blank blob + { + name: "make() should produce an empty antestone blob of length 0", + run() { + let b = blob.make(); + let isBlob = blob["isblob"](b); + let length = blob.length(b); + let passed = (isBlob === true && length === 0); + let messages = []; + if (!isBlob) messages.push("Returned object is not recognized as a blob"); + if (length !== 0) messages.push(`Expected length 0, got ${length}`); + return { passed, messages }; + } + }, + + // 2) Make a blob with some capacity + { + name: "make(16) should create a blob with capacity >=16 bits and length=0", + run() { + let b = blob.make(16); + let isBlob = blob["isblob"](b); + let length = blob.length(b); + let passed = isBlob && length === 0; + let messages = []; + if (!isBlob) messages.push("Not recognized as a blob"); + if (length !== 0) messages.push(`Expected length=0, got ${length}`); + return { passed, messages }; + } + }, + + // 3) Make a blob with (length, logical) + { + name: "make(5, true) should create a blob of length=5 bits, all 1s (antestone)", + run() { + let b = blob.make(5, true); + let len = blob.length(b); + if (len !== 5) { + return { + passed: false, + messages: [`Expected length=5, got ${len}`] + }; + } + // Check bits + for (let i = 0; i < 5; i++) { + let bitVal = blob.read_logical(b, i); + if (bitVal !== true) { + return { + passed: false, + messages: [`Bit at index ${i} expected true, got ${bitVal}`] + }; + } + } + return { passed: true, messages: [] }; + } + }, + + // 4) Write bits to an empty blob + { + name: "write_bit() on an empty blob, then read_logical() to verify bits", + run() { + let b = blob.make(); // starts length=0 + // write bits: true, false, true + blob.write_bit(b, true); // bit #0 + blob.write_bit(b, false); // bit #1 + blob.write_bit(b, true); // bit #2 + let len = blob.length(b); + if (len !== 3) { + return { + passed: false, + messages: [`Expected length=3, got ${len}`] + }; + } + let bits = [ + blob.read_logical(b, 0), + blob.read_logical(b, 1), + blob.read_logical(b, 2) + ]; + let compare = deepCompare([true, false, true], bits); + return compare; + } + }, + + // 5) Stone a blob, then attempt to write -> fail + { + name: "Stoning a blob should prevent further writes", + run() { + let b = blob.make(5, false); + // Stone it + blob.stone(b); + // Try to write + let passed = true; + let messages = []; + try { + blob.write_bit(b, true); + passed = false; + messages.push("Expected an error or refusal when writing to a stone blob, but none occurred"); + } catch (e) { + // We expect an exception or some error scenario + } + return { passed, messages }; + } + }, + + // 6) make(blob, from, to) - copying range from an existing blob + { + name: "make(existing_blob, from, to) can copy partial range", + run() { + // Create a 10-bit blob: pattern T F T F T F T F T F + let original = blob.make(); + for (let i = 0; i < 10; i++) { + blob.write_bit(original, i % 2 === 0); + } + // Copy bits [2..7) + // That slice is bits #2..6: T, F, T, F, T + // indices: 2: T(1), 3: F(0), 4: T(1), 5: F(0), 6: T(1) + // so length=5 + let copy = blob.make(original, 2, 7); + let len = blob.length(copy); + if (len !== 5) { + return { + passed: false, + messages: [`Expected length=5, got ${len}`] + }; + } + let bits = []; + for (let i = 0; i < len; i++) { + bits.push(blob.read_logical(copy, i)); + } + let compare = deepCompare([true, false, true, false, true], bits); + return compare; + } + }, + + // 7) Checking isblob(something) + { + name: "isblob should correctly identify blob vs. non-blob", + run() { + let b = blob.make(); + let isB = blob["isblob"](b); + let isNum = blob["isblob"](42); + let isObj = blob["isblob"]({ length: 3 }); + let passed = (isB === true && isNum === false && isObj === false); + let messages = []; + if (!passed) { + messages.push(`Expected isblob(b)=true, isblob(42)=false, isblob({})=false; got ${isB}, ${isNum}, ${isObj}`); + } + return { passed, messages }; + } + } +]; + +// --------------------------------------------------------------------------- +// Run all tests +// --------------------------------------------------------------------------- +let results = []; +for (let i = 0; i < tests.length; i++) { + let { name, run } = tests[i]; + let result = runTest(name, run); + results.push(result); +} + +// Print results +let passedCount = 0; +for (let r of results) { + let status = r.passed ? "Passed" : "Failed"; + console.log(`${r.testName} - ${status}`); + if (!r.passed && r.messages.length > 0) { + console.log(" " + r.messages.join("\n ")); + } + if (r.passed) passedCount++; +} + +console.log(`\nResult: ${passedCount}/${results.length} tests passed`); +if (passedCount < results.length) { + console.log("Overall: FAILED"); + if (os && os.exit) os.exit(1); +} else { + console.log("Overall: PASSED"); + if (os && os.exit) os.exit(0); +} diff --git a/tests/window.js b/tests/window.js index 572213e5..9a30d3e2 100644 --- a/tests/window.js +++ b/tests/window.js @@ -1,3 +1,5 @@ +//var draw = use('draw2d') + prosperon.win = prosperon.engine_start({ title:`Prosperon [${prosperon.version}-${prosperon.revision}]`, width: 1280, @@ -19,13 +21,31 @@ prosperon.win = prosperon.engine_start({ url: "https://prosperon.dev" }) +var ren = prosperon.win.make_renderer("opengl") + function loop() { + ren.clear() + ren.fillrect({x:50,y:50,height:50,width:50}) + ren.present() $_.delay(loop, 1/60) } loop() $_.delay($_.stop, 3) -$_.receiver(e => { - console.log(json.encode(e)) +var os = use('os') +var ioguy = { + __ACTORDATA__: { + id: os.ioactor() + } +} + +$_.send(ioguy, { + type: "subscribe", + actor: $_ +}) + +$_.receiver(e => { + if (e.type === 'quit') + os.quit() })