add blob; pull out crypto, time; add sdl_renderer
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
Some checks failed
Build and Deploy / package-dist (push) Has been cancelled
Build and Deploy / deploy-itch (push) Has been cancelled
Build and Deploy / deploy-gitea (push) Has been cancelled
Build and Deploy / build-windows (CLANG64) (push) Has been cancelled
Build and Deploy / build-linux (push) Has been cancelled
This commit is contained in:
@@ -138,7 +138,7 @@ deps += dependency('soloud', static:true)
|
|||||||
deps += dependency('libqrencode', static: true)
|
deps += dependency('libqrencode', static: true)
|
||||||
|
|
||||||
sources = []
|
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
|
# quirc src
|
||||||
src += ['thirdparty/quirc/quirc.c', 'thirdparty/quirc/decode.c','thirdparty/quirc/identify.c', 'thirdparty/quirc/version_db.c']
|
src += ['thirdparty/quirc/quirc.c', 'thirdparty/quirc/decode.c','thirdparty/quirc/identify.c', 'thirdparty/quirc/version_db.c']
|
||||||
|
|||||||
@@ -87,8 +87,6 @@ prosperon.PATH = [
|
|||||||
"scripts/modules/ext/",
|
"scripts/modules/ext/",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// path is the path of a module or script to resolve
|
// path is the path of a module or script to resolve
|
||||||
var script_fn = function script_fn(path) {
|
var script_fn = function script_fn(path) {
|
||||||
var parsed = {}
|
var parsed = {}
|
||||||
@@ -225,8 +223,6 @@ console[prosperon.DOC] = {
|
|||||||
clear: "Clear console."
|
clear: "Clear console."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var BASEPATH = 'scripts/core/base.js'
|
var BASEPATH = 'scripts/core/base.js'
|
||||||
var script = io.slurp(BASEPATH)
|
var script = io.slurp(BASEPATH)
|
||||||
var fnname = "base"
|
var fnname = "base"
|
||||||
@@ -288,8 +284,6 @@ globalThis.use = function use(file) {
|
|||||||
return use_cache[file]
|
return use_cache[file]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
globalThis.json = use('json')
|
globalThis.json = use('json')
|
||||||
var time = use('time')
|
var time = use('time')
|
||||||
|
|
||||||
@@ -297,8 +291,9 @@ function parse_file(content, file) {
|
|||||||
if (!content) return {}
|
if (!content) return {}
|
||||||
if (!/^\s*---\s*$/m.test(content)) {
|
if (!/^\s*---\s*$/m.test(content)) {
|
||||||
var part = content.trim()
|
var part = content.trim()
|
||||||
if (part.match(/return\s+[^;]+;?\s*$/))
|
if (part.match(/return\s+[^;]+;?\s*$/)) {
|
||||||
return { module: part }
|
return { module: part }
|
||||||
|
}
|
||||||
return { program: part }
|
return { program: part }
|
||||||
}
|
}
|
||||||
var parts = content.split(/\n\s*---\s*\n/)
|
var parts = content.split(/\n\s*---\s*\n/)
|
||||||
@@ -420,7 +415,6 @@ function cant_kill() {
|
|||||||
actor.toString = function() { return this[FILE] }
|
actor.toString = function() { return this[FILE] }
|
||||||
|
|
||||||
actor.spawn = function spawn(script, config) {
|
actor.spawn = function spawn(script, config) {
|
||||||
|
|
||||||
if (this[DEAD]) throw new Error("Attempting to spawn on a dead actor")
|
if (this[DEAD]) throw new Error("Attempting to spawn on a dead actor")
|
||||||
var prog
|
var prog
|
||||||
if (!script) {
|
if (!script) {
|
||||||
@@ -536,17 +530,12 @@ actor[UNDERLINGS] = new Set()
|
|||||||
|
|
||||||
globalThis.mixin("color")
|
globalThis.mixin("color")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var DOCPATH = 'scripts/core/doc.js'
|
var DOCPATH = 'scripts/core/doc.js'
|
||||||
var script = io.slurp(DOCPATH)
|
var script = io.slurp(DOCPATH)
|
||||||
var fnname = "doc"
|
var fnname = "doc"
|
||||||
script = `(function ${fnname}() { ${script}; })`
|
script = `(function ${fnname}() { ${script}; })`
|
||||||
//js.eval(DOCPATH, script)()
|
//js.eval(DOCPATH, script)()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
When handling a message, the message appears like this:
|
When handling a message, the message appears like this:
|
||||||
{
|
{
|
||||||
@@ -949,6 +938,10 @@ function handle_message(msg) {
|
|||||||
case "greet":
|
case "greet":
|
||||||
var greeter = greeters[msg.id]
|
var greeter = greeters[msg.id]
|
||||||
if (greeter) greeter({type: "actor_started", actor: create_actor(msg)})
|
if (greeter) greeter({type: "actor_started", actor: create_actor(msg)})
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (receive_fn) receive_fn(msg)
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ var subscribers = []
|
|||||||
$_.receiver(e => {
|
$_.receiver(e => {
|
||||||
if (e.type === "subscribe") {
|
if (e.type === "subscribe") {
|
||||||
if (!e.actor) throw Error('Got a subscribe message with no actor.');
|
if (!e.actor) throw Error('Got a subscribe message with no actor.');
|
||||||
|
console.log('subscribing: ' + json.encode(e.actor))
|
||||||
subscribers.push(e.actor)
|
subscribers.push(e.actor)
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var a of subscribers)
|
for (var a of subscribers)
|
||||||
$_.send(a, e)
|
$_.send(a, e);
|
||||||
});
|
});
|
||||||
|
|||||||
841
scripts/modules/sdl_render.js
Normal file
841
scripts/modules/sdl_render.js
Normal file
@@ -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
|
||||||
271
source/jsffi.c
271
source/jsffi.c
@@ -1014,6 +1014,8 @@ void SDL_Window_free(JSRuntime *rt, SDL_Window *w)
|
|||||||
SDL_DestroyWindow(w);
|
SDL_DestroyWindow(w);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
void SDL_Renderer_free(JSRuntime *rt, SDL_Renderer *r)
|
void SDL_Renderer_free(JSRuntime *rt, SDL_Renderer *r)
|
||||||
{
|
{
|
||||||
SDL_DestroyRenderer(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_GPUComputePass_free(JSRuntime *rt, SDL_GPUComputePass *c) { }
|
||||||
void SDL_GPUCopyPass_free(JSRuntime *rt, SDL_GPUCopyPass *c) { }
|
void SDL_GPUCopyPass_free(JSRuntime *rt, SDL_GPUCopyPass *c) { }
|
||||||
@@ -1082,7 +1082,6 @@ QJSCLASSMARK(transform,)
|
|||||||
QJSCLASS(font,)
|
QJSCLASS(font,)
|
||||||
QJSCLASS(datastream,)
|
QJSCLASS(datastream,)
|
||||||
QJSCLASS(SDL_Window,)
|
QJSCLASS(SDL_Window,)
|
||||||
QJSCLASS(SDL_Renderer,)
|
|
||||||
QJSCLASS(SDL_Camera,)
|
QJSCLASS(SDL_Camera,)
|
||||||
|
|
||||||
void SDL_Texture_free(JSRuntime *rt, SDL_Texture *t){
|
void SDL_Texture_free(JSRuntime *rt, SDL_Texture *t){
|
||||||
@@ -1100,7 +1099,6 @@ QJSCLASS(SDL_Surface,
|
|||||||
)
|
)
|
||||||
|
|
||||||
QJSCLASS(SDL_GPUDevice,)
|
QJSCLASS(SDL_GPUDevice,)
|
||||||
QJSCLASS(SDL_Thread,)
|
|
||||||
|
|
||||||
GPURELEASECLASS(Buffer)
|
GPURELEASECLASS(Buffer)
|
||||||
GPURELEASECLASS(ComputePipeline)
|
GPURELEASECLASS(ComputePipeline)
|
||||||
@@ -5182,15 +5180,6 @@ static const JSCFunctionListEntry js_SDL_Surface_funcs[] = {
|
|||||||
MIST_FUNC_DEF(surface, dup, 0),
|
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,
|
JSC_CCALL(camera_frame,
|
||||||
SDL_ClearError();
|
SDL_ClearError();
|
||||||
SDL_Camera *cam = js2SDL_Camera(js,self);
|
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());
|
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,
|
JSC_SCALL(console_print,
|
||||||
printf("%s", str);
|
printf("%s", str);
|
||||||
)
|
)
|
||||||
@@ -6854,6 +6819,7 @@ static const JSCFunctionListEntry js_os_funcs[] = {
|
|||||||
MIST_FUNC_DEF(os, register_actor, 2),
|
MIST_FUNC_DEF(os, register_actor, 2),
|
||||||
MIST_FUNC_DEF(os, unneeded, 2),
|
MIST_FUNC_DEF(os, unneeded, 2),
|
||||||
MIST_FUNC_DEF(os, destroy, 0),
|
MIST_FUNC_DEF(os, destroy, 0),
|
||||||
|
MIST_FUNC_DEF(os, ioactor, 0),
|
||||||
};
|
};
|
||||||
|
|
||||||
JSC_CCALL(js_dump_class, return js_get_object_class_distribution(js))
|
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);
|
JSValue js_tracy_use(JSContext *js);
|
||||||
#endif
|
#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 <stdint.h>
|
|
||||||
#include <stddef.h>
|
|
||||||
|
|
||||||
#if defined(_WIN32)
|
|
||||||
// ------- Windows: use BCryptGenRandom -------
|
|
||||||
#include <windows.h>
|
|
||||||
#include <bcrypt.h>
|
|
||||||
|
|
||||||
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 <unistd.h>
|
|
||||||
#include <sys/syscall.h>
|
|
||||||
#include <fcntl.h>
|
|
||||||
#include <errno.h>
|
|
||||||
|
|
||||||
// If we have a new enough libc and kernel, getrandom is available.
|
|
||||||
// Otherwise, we’ll do a /dev/urandom fallback.
|
|
||||||
#include <sys/stat.h>
|
|
||||||
|
|
||||||
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 <fcntl.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
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(io)
|
||||||
MISTUSE(os)
|
MISTUSE(os)
|
||||||
MISTUSE(input)
|
MISTUSE(input)
|
||||||
MISTUSE(time)
|
|
||||||
MISTUSE(math)
|
MISTUSE(math)
|
||||||
MISTUSE(spline)
|
MISTUSE(spline)
|
||||||
MISTUSE(geometry)
|
MISTUSE(geometry)
|
||||||
@@ -7392,7 +7143,10 @@ MISTUSE(util)
|
|||||||
MISTUSE(video)
|
MISTUSE(video)
|
||||||
MISTUSE(camera)
|
MISTUSE(camera)
|
||||||
MISTUSE(debug)
|
MISTUSE(debug)
|
||||||
MISTUSE(crypto)
|
|
||||||
|
#include "qjs_crypto.h"
|
||||||
|
#include "qjs_time.h"
|
||||||
|
#include "qjs_blob.h"
|
||||||
|
|
||||||
JSValue js_imgui_use(JSContext *js);
|
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(qr));
|
||||||
arrput(rt->module_registry, MISTLINE(wota));
|
arrput(rt->module_registry, MISTLINE(wota));
|
||||||
arrput(rt->module_registry, MISTLINE(crypto));
|
arrput(rt->module_registry, MISTLINE(crypto));
|
||||||
|
arrput(rt->module_registry, MISTLINE(blob));
|
||||||
|
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
arrput(rt->module_registry, MISTLINE(tracy));
|
arrput(rt->module_registry, MISTLINE(tracy));
|
||||||
@@ -7443,17 +7198,19 @@ void ffi_load(JSContext *js)
|
|||||||
QJSCLASSPREP_FUNCS(rtree)
|
QJSCLASSPREP_FUNCS(rtree)
|
||||||
QJSCLASSPREP_FUNCS(SDL_Window)
|
QJSCLASSPREP_FUNCS(SDL_Window)
|
||||||
QJSCLASSPREP_FUNCS(SDL_Surface)
|
QJSCLASSPREP_FUNCS(SDL_Surface)
|
||||||
QJSCLASSPREP_FUNCS(SDL_Thread)
|
|
||||||
QJSCLASSPREP_FUNCS(SDL_Texture)
|
QJSCLASSPREP_FUNCS(SDL_Texture)
|
||||||
QJSCLASSPREP_FUNCS(SDL_Renderer)
|
QJSCLASSPREP_NO_FUNCS(SDL_Cursor)
|
||||||
QJSCLASSPREP_FUNCS(SDL_Camera)
|
QJSCLASSPREP_FUNCS(SDL_Camera)
|
||||||
|
|
||||||
|
QJSCLASSPREP_FUNCS(SDL_Renderer)
|
||||||
|
|
||||||
QJSCLASSPREP_FUNCS(SDL_GPUDevice)
|
QJSCLASSPREP_FUNCS(SDL_GPUDevice)
|
||||||
QJSCLASSPREP_FUNCS(SDL_GPUTexture)
|
QJSCLASSPREP_FUNCS(SDL_GPUTexture)
|
||||||
QJSCLASSPREP_FUNCS(SDL_GPUCommandBuffer)
|
QJSCLASSPREP_FUNCS(SDL_GPUCommandBuffer)
|
||||||
QJSCLASSPREP_FUNCS(SDL_GPURenderPass)
|
QJSCLASSPREP_FUNCS(SDL_GPURenderPass)
|
||||||
QJSCLASSPREP_FUNCS(SDL_GPUComputePass)
|
QJSCLASSPREP_FUNCS(SDL_GPUComputePass)
|
||||||
|
|
||||||
QJSCLASSPREP_NO_FUNCS(SDL_Cursor)
|
|
||||||
QJSCLASSPREP_NO_FUNCS(SDL_GPUCopyPass)
|
QJSCLASSPREP_NO_FUNCS(SDL_GPUCopyPass)
|
||||||
QJSCLASSPREP_NO_FUNCS(SDL_GPUFence)
|
QJSCLASSPREP_NO_FUNCS(SDL_GPUFence)
|
||||||
QJSCLASSPREP_NO_FUNCS(SDL_GPUTransferBuffer)
|
QJSCLASSPREP_NO_FUNCS(SDL_GPUTransferBuffer)
|
||||||
|
|||||||
@@ -474,6 +474,8 @@ void actor_free(prosperon_rt *actor)
|
|||||||
/* If still present, free each JSValue. */
|
/* If still present, free each JSValue. */
|
||||||
for (int i = 0; i < arrlen(actor->events); i++)
|
for (int i = 0; i < arrlen(actor->events); i++)
|
||||||
JS_FreeValue(js, actor->events[i]);
|
JS_FreeValue(js, actor->events[i]);
|
||||||
|
|
||||||
|
printf("FREEIN ACTOR EVENTS\n");
|
||||||
arrfree(actor->events);
|
arrfree(actor->events);
|
||||||
|
|
||||||
JSRuntime *rt = JS_GetRuntime(js);
|
JSRuntime *rt = JS_GetRuntime(js);
|
||||||
@@ -1352,17 +1354,17 @@ int main(int argc, char **argv)
|
|||||||
queue_cond = SDL_CreateCondition();
|
queue_cond = SDL_CreateCondition();
|
||||||
actors_mutex = SDL_CreateMutex();
|
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";
|
const char *io_script = "scripts/core/io.js";
|
||||||
char **io_argv = malloc(sizeof(char*)*2);
|
char **io_argv = malloc(sizeof(char*)*2);
|
||||||
io_argv[0] = strdup(argv[0]);
|
io_argv[0] = strdup(argv[0]);
|
||||||
io_argv[1] = strdup(io_script);
|
io_argv[1] = strdup(io_script);
|
||||||
io_actor = create_actor(2, io_argv);
|
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. */
|
/* Start the thread that pumps ready actors, one per logical core. */
|
||||||
for (int i = 0; i < cores; i++) {
|
for (int i = 0; i < cores; i++) {
|
||||||
char threadname[128];
|
char threadname[128];
|
||||||
@@ -1383,8 +1385,9 @@ int main(int argc, char **argv)
|
|||||||
if (event.type == queue_event)
|
if (event.type == queue_event)
|
||||||
goto QUEUE;
|
goto QUEUE;
|
||||||
|
|
||||||
WotaBuffer wb = event2wota(&event);
|
// WotaBuffer wb = event2wota(&event);
|
||||||
send_message(io_actor->id, wb.data);
|
// send_message(io_actor->id, wb.data);
|
||||||
|
continue;
|
||||||
|
|
||||||
QUEUE:
|
QUEUE:
|
||||||
SDL_LockMutex(queue_mutex);
|
SDL_LockMutex(queue_mutex);
|
||||||
@@ -1411,3 +1414,8 @@ int actor_exists(char *id)
|
|||||||
else
|
else
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JSValue js_os_ioactor(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||||
|
{
|
||||||
|
return JS_NewString(js, io_actor->id);
|
||||||
|
}
|
||||||
|
|||||||
@@ -84,4 +84,6 @@ void set_actor_state(prosperon_rt *actor);
|
|||||||
|
|
||||||
int prosperon_mount_core(void);
|
int prosperon_mount_core(void);
|
||||||
|
|
||||||
|
JSValue js_os_ioactor(JSContext *js, JSValue self, int argc, JSValue *argv);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
460
source/qjs_blob.c
Normal file
460
source/qjs_blob.c
Normal file
@@ -0,0 +1,460 @@
|
|||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
#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;
|
||||||
|
}
|
||||||
8
source/qjs_blob.h
Normal file
8
source/qjs_blob.h
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#ifndef QJS_BLOB_H
|
||||||
|
#define QJS_BLOB_H
|
||||||
|
|
||||||
|
#include <quickjs.h>
|
||||||
|
|
||||||
|
JSValue js_blob_use(JSContext *ctx);
|
||||||
|
|
||||||
|
#endif
|
||||||
227
source/qjs_crypto.c
Normal file
227
source/qjs_crypto.c
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
#include "qjs_crypto.h"
|
||||||
|
#include "quickjs.h"
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#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 <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
// ------- Windows: use BCryptGenRandom -------
|
||||||
|
#include <windows.h>
|
||||||
|
#include <bcrypt.h>
|
||||||
|
|
||||||
|
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 <unistd.h>
|
||||||
|
#include <sys/syscall.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <errno.h>
|
||||||
|
|
||||||
|
// If we have a new enough libc and kernel, getrandom is available.
|
||||||
|
// Otherwise, we’ll do a /dev/urandom fallback.
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
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 <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
8
source/qjs_crypto.h
Normal file
8
source/qjs_crypto.h
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#ifndef QJS_CRYPTO_H
|
||||||
|
#define QJS_CRYPTO_H
|
||||||
|
|
||||||
|
#include "quickjs.h"
|
||||||
|
|
||||||
|
JSValue js_crypto_use(JSContext *ctx);
|
||||||
|
|
||||||
|
#endif
|
||||||
506
source/qjs_renderer.c
Normal file
506
source/qjs_renderer.c
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
#include "qjs_renderer.h"
|
||||||
|
#include "quickjs.h"
|
||||||
|
|
||||||
|
#include <SDL3/SDL.h>
|
||||||
|
#include "qjs_macros.h"
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#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;
|
||||||
|
}
|
||||||
8
source/qjs_renderer.h
Normal file
8
source/qjs_renderer.h
Normal file
@@ -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 */
|
||||||
45
source/qjs_time.c
Normal file
45
source/qjs_time.c
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#include "qjs_time.h"
|
||||||
|
#include "quickjs.h"
|
||||||
|
#include <time.h> // For time() calls, localtime, etc.
|
||||||
|
#include <sys/time.h> // 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;
|
||||||
|
}
|
||||||
8
source/qjs_time.h
Normal file
8
source/qjs_time.h
Normal file
@@ -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 */
|
||||||
321
tests/blob.js
Normal file
321
tests/blob.js
Normal file
@@ -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);
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
//var draw = use('draw2d')
|
||||||
|
|
||||||
prosperon.win = prosperon.engine_start({
|
prosperon.win = prosperon.engine_start({
|
||||||
title:`Prosperon [${prosperon.version}-${prosperon.revision}]`,
|
title:`Prosperon [${prosperon.version}-${prosperon.revision}]`,
|
||||||
width: 1280,
|
width: 1280,
|
||||||
@@ -19,13 +21,31 @@ prosperon.win = prosperon.engine_start({
|
|||||||
url: "https://prosperon.dev"
|
url: "https://prosperon.dev"
|
||||||
})
|
})
|
||||||
|
|
||||||
|
var ren = prosperon.win.make_renderer("opengl")
|
||||||
|
|
||||||
function loop() {
|
function loop() {
|
||||||
|
ren.clear()
|
||||||
|
ren.fillrect({x:50,y:50,height:50,width:50})
|
||||||
|
ren.present()
|
||||||
$_.delay(loop, 1/60)
|
$_.delay(loop, 1/60)
|
||||||
}
|
}
|
||||||
loop()
|
loop()
|
||||||
|
|
||||||
$_.delay($_.stop, 3)
|
$_.delay($_.stop, 3)
|
||||||
|
|
||||||
$_.receiver(e => {
|
var os = use('os')
|
||||||
console.log(json.encode(e))
|
var ioguy = {
|
||||||
|
__ACTORDATA__: {
|
||||||
|
id: os.ioactor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$_.send(ioguy, {
|
||||||
|
type: "subscribe",
|
||||||
|
actor: $_
|
||||||
|
})
|
||||||
|
|
||||||
|
$_.receiver(e => {
|
||||||
|
if (e.type === 'quit')
|
||||||
|
os.quit()
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user