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)
|
||||
|
||||
sources = []
|
||||
src += ['anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c','render.c','simplex.c','spline.c', 'transform.c','prosperon.c', 'wildmatch.c', 'sprite.c', 'rtree.c', 'qjs_dmon.c', 'qjs_nota.c', 'qjs_enet.c', 'qjs_soloud.c', 'qjs_qr.c', 'qjs_wota.c', 'monocypher.c']
|
||||
src += ['anim.c', 'config.c', 'datastream.c','font.c','HandmadeMath.c','jsffi.c','model.c','render.c','simplex.c','spline.c', 'transform.c','prosperon.c', 'wildmatch.c', 'sprite.c', 'rtree.c', 'qjs_dmon.c', 'qjs_nota.c', 'qjs_enet.c', 'qjs_soloud.c', 'qjs_qr.c', 'qjs_wota.c', 'monocypher.c', 'qjs_blob.c', 'qjs_crypto.c', 'qjs_time.c']
|
||||
|
||||
# quirc src
|
||||
src += ['thirdparty/quirc/quirc.c', 'thirdparty/quirc/decode.c','thirdparty/quirc/identify.c', 'thirdparty/quirc/version_db.c']
|
||||
|
||||
@@ -87,8 +87,6 @@ prosperon.PATH = [
|
||||
"scripts/modules/ext/",
|
||||
]
|
||||
|
||||
|
||||
|
||||
// path is the path of a module or script to resolve
|
||||
var script_fn = function script_fn(path) {
|
||||
var parsed = {}
|
||||
@@ -225,8 +223,6 @@ console[prosperon.DOC] = {
|
||||
clear: "Clear console."
|
||||
}
|
||||
|
||||
|
||||
|
||||
var BASEPATH = 'scripts/core/base.js'
|
||||
var script = io.slurp(BASEPATH)
|
||||
var fnname = "base"
|
||||
@@ -288,8 +284,6 @@ globalThis.use = function use(file) {
|
||||
return use_cache[file]
|
||||
}
|
||||
|
||||
|
||||
|
||||
globalThis.json = use('json')
|
||||
var time = use('time')
|
||||
|
||||
@@ -297,8 +291,9 @@ function parse_file(content, file) {
|
||||
if (!content) return {}
|
||||
if (!/^\s*---\s*$/m.test(content)) {
|
||||
var part = content.trim()
|
||||
if (part.match(/return\s+[^;]+;?\s*$/))
|
||||
if (part.match(/return\s+[^;]+;?\s*$/)) {
|
||||
return { module: part }
|
||||
}
|
||||
return { program: part }
|
||||
}
|
||||
var parts = content.split(/\n\s*---\s*\n/)
|
||||
@@ -420,7 +415,6 @@ function cant_kill() {
|
||||
actor.toString = function() { return this[FILE] }
|
||||
|
||||
actor.spawn = function spawn(script, config) {
|
||||
|
||||
if (this[DEAD]) throw new Error("Attempting to spawn on a dead actor")
|
||||
var prog
|
||||
if (!script) {
|
||||
@@ -536,17 +530,12 @@ actor[UNDERLINGS] = new Set()
|
||||
|
||||
globalThis.mixin("color")
|
||||
|
||||
|
||||
|
||||
var DOCPATH = 'scripts/core/doc.js'
|
||||
var script = io.slurp(DOCPATH)
|
||||
var fnname = "doc"
|
||||
script = `(function ${fnname}() { ${script}; })`
|
||||
//js.eval(DOCPATH, script)()
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
When handling a message, the message appears like this:
|
||||
{
|
||||
@@ -949,6 +938,10 @@ function handle_message(msg) {
|
||||
case "greet":
|
||||
var greeter = greeters[msg.id]
|
||||
if (greeter) greeter({type: "actor_started", actor: create_actor(msg)})
|
||||
break;
|
||||
default:
|
||||
if (receive_fn) receive_fn(msg)
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -6,10 +6,11 @@ var subscribers = []
|
||||
$_.receiver(e => {
|
||||
if (e.type === "subscribe") {
|
||||
if (!e.actor) throw Error('Got a subscribe message with no actor.');
|
||||
console.log('subscribing: ' + json.encode(e.actor))
|
||||
subscribers.push(e.actor)
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
for (var a of subscribers)
|
||||
$_.send(a, e)
|
||||
$_.send(a, e);
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
void SDL_Renderer_free(JSRuntime *rt, SDL_Renderer *r)
|
||||
{
|
||||
SDL_DestroyRenderer(r);
|
||||
@@ -1044,9 +1046,7 @@ void SDL_GPUCommandBuffer_free(JSRuntime *rt, SDL_GPUCommandBuffer *c)
|
||||
{
|
||||
}
|
||||
|
||||
void SDL_Thread_free(JSRuntime *rt, SDL_Thread *t)
|
||||
{
|
||||
}
|
||||
QJSCLASS(SDL_Renderer,)
|
||||
|
||||
void SDL_GPUComputePass_free(JSRuntime *rt, SDL_GPUComputePass *c) { }
|
||||
void SDL_GPUCopyPass_free(JSRuntime *rt, SDL_GPUCopyPass *c) { }
|
||||
@@ -1082,7 +1082,6 @@ QJSCLASSMARK(transform,)
|
||||
QJSCLASS(font,)
|
||||
QJSCLASS(datastream,)
|
||||
QJSCLASS(SDL_Window,)
|
||||
QJSCLASS(SDL_Renderer,)
|
||||
QJSCLASS(SDL_Camera,)
|
||||
|
||||
void SDL_Texture_free(JSRuntime *rt, SDL_Texture *t){
|
||||
@@ -1100,7 +1099,6 @@ QJSCLASS(SDL_Surface,
|
||||
)
|
||||
|
||||
QJSCLASS(SDL_GPUDevice,)
|
||||
QJSCLASS(SDL_Thread,)
|
||||
|
||||
GPURELEASECLASS(Buffer)
|
||||
GPURELEASECLASS(ComputePipeline)
|
||||
@@ -5182,15 +5180,6 @@ static const JSCFunctionListEntry js_SDL_Surface_funcs[] = {
|
||||
MIST_FUNC_DEF(surface, dup, 0),
|
||||
};
|
||||
|
||||
JSC_CCALL(thread_wait,
|
||||
SDL_Thread *th = js2SDL_Thread(js,self);
|
||||
SDL_WaitThread(th, NULL);
|
||||
)
|
||||
|
||||
static const JSCFunctionListEntry js_SDL_Thread_funcs[] = {
|
||||
MIST_FUNC_DEF(thread,wait,0),
|
||||
};
|
||||
|
||||
JSC_CCALL(camera_frame,
|
||||
SDL_ClearError();
|
||||
SDL_Camera *cam = js2SDL_Camera(js,self);
|
||||
@@ -5315,30 +5304,6 @@ JSC_SCALL(os_openurl,
|
||||
ret = JS_ThrowReferenceError(js, "unable to open url %s: %s\n", str, SDL_GetError());
|
||||
)
|
||||
|
||||
JSC_CCALL(time_now,
|
||||
struct timeval ct;
|
||||
gettimeofday(&ct, NULL);
|
||||
return number2js(js,(double)ct.tv_sec+(double)(ct.tv_usec/1000000.0));
|
||||
)
|
||||
|
||||
JSValue js_time_computer_dst(JSContext *js, JSValue self) {
|
||||
time_t t = time(NULL);
|
||||
return JS_NewBool(js,localtime(&t)->tm_isdst);
|
||||
}
|
||||
|
||||
JSValue js_time_computer_zone(JSContext *js, JSValue self) {
|
||||
time_t t = time(NULL);
|
||||
time_t local_t = mktime(localtime(&t));
|
||||
double diff = difftime(t, local_t);
|
||||
return number2js(js,diff/3600);
|
||||
}
|
||||
|
||||
static const JSCFunctionListEntry js_time_funcs[] = {
|
||||
MIST_FUNC_DEF(time, now, 0),
|
||||
MIST_FUNC_DEF(time, computer_dst, 0),
|
||||
MIST_FUNC_DEF(time, computer_zone, 0)
|
||||
};
|
||||
|
||||
JSC_SCALL(console_print,
|
||||
printf("%s", str);
|
||||
)
|
||||
@@ -6854,6 +6819,7 @@ static const JSCFunctionListEntry js_os_funcs[] = {
|
||||
MIST_FUNC_DEF(os, register_actor, 2),
|
||||
MIST_FUNC_DEF(os, unneeded, 2),
|
||||
MIST_FUNC_DEF(os, destroy, 0),
|
||||
MIST_FUNC_DEF(os, ioactor, 0),
|
||||
};
|
||||
|
||||
JSC_CCALL(js_dump_class, return js_get_object_class_distribution(js))
|
||||
@@ -7165,224 +7131,9 @@ JSValue js_miniz_use(JSContext *js);
|
||||
JSValue js_tracy_use(JSContext *js);
|
||||
#endif
|
||||
|
||||
#include "monocypher.h"
|
||||
|
||||
// randombytes.c - Minimal cross-platform CSPRNG shim (single file)
|
||||
/*
|
||||
Usage:
|
||||
#include "randombytes.c"
|
||||
|
||||
int main() {
|
||||
uint8_t buffer[32];
|
||||
if (randombytes(buffer, sizeof(buffer)) != 0) {
|
||||
// handle error
|
||||
}
|
||||
// buffer now has 32 cryptographically secure random bytes
|
||||
}
|
||||
*/
|
||||
|
||||
#include <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(os)
|
||||
MISTUSE(input)
|
||||
MISTUSE(time)
|
||||
MISTUSE(math)
|
||||
MISTUSE(spline)
|
||||
MISTUSE(geometry)
|
||||
@@ -7392,7 +7143,10 @@ MISTUSE(util)
|
||||
MISTUSE(video)
|
||||
MISTUSE(camera)
|
||||
MISTUSE(debug)
|
||||
MISTUSE(crypto)
|
||||
|
||||
#include "qjs_crypto.h"
|
||||
#include "qjs_time.h"
|
||||
#include "qjs_blob.h"
|
||||
|
||||
JSValue js_imgui_use(JSContext *js);
|
||||
|
||||
@@ -7428,6 +7182,7 @@ void ffi_load(JSContext *js)
|
||||
arrput(rt->module_registry, MISTLINE(qr));
|
||||
arrput(rt->module_registry, MISTLINE(wota));
|
||||
arrput(rt->module_registry, MISTLINE(crypto));
|
||||
arrput(rt->module_registry, MISTLINE(blob));
|
||||
|
||||
#ifdef TRACY_ENABLE
|
||||
arrput(rt->module_registry, MISTLINE(tracy));
|
||||
@@ -7443,17 +7198,19 @@ void ffi_load(JSContext *js)
|
||||
QJSCLASSPREP_FUNCS(rtree)
|
||||
QJSCLASSPREP_FUNCS(SDL_Window)
|
||||
QJSCLASSPREP_FUNCS(SDL_Surface)
|
||||
QJSCLASSPREP_FUNCS(SDL_Thread)
|
||||
QJSCLASSPREP_FUNCS(SDL_Texture)
|
||||
QJSCLASSPREP_FUNCS(SDL_Renderer)
|
||||
QJSCLASSPREP_NO_FUNCS(SDL_Cursor)
|
||||
QJSCLASSPREP_FUNCS(SDL_Camera)
|
||||
|
||||
QJSCLASSPREP_FUNCS(SDL_Renderer)
|
||||
|
||||
QJSCLASSPREP_FUNCS(SDL_GPUDevice)
|
||||
QJSCLASSPREP_FUNCS(SDL_GPUTexture)
|
||||
QJSCLASSPREP_FUNCS(SDL_GPUCommandBuffer)
|
||||
QJSCLASSPREP_FUNCS(SDL_GPURenderPass)
|
||||
QJSCLASSPREP_FUNCS(SDL_GPUComputePass)
|
||||
|
||||
QJSCLASSPREP_NO_FUNCS(SDL_Cursor)
|
||||
|
||||
QJSCLASSPREP_NO_FUNCS(SDL_GPUCopyPass)
|
||||
QJSCLASSPREP_NO_FUNCS(SDL_GPUFence)
|
||||
QJSCLASSPREP_NO_FUNCS(SDL_GPUTransferBuffer)
|
||||
|
||||
@@ -474,6 +474,8 @@ void actor_free(prosperon_rt *actor)
|
||||
/* If still present, free each JSValue. */
|
||||
for (int i = 0; i < arrlen(actor->events); i++)
|
||||
JS_FreeValue(js, actor->events[i]);
|
||||
|
||||
printf("FREEIN ACTOR EVENTS\n");
|
||||
arrfree(actor->events);
|
||||
|
||||
JSRuntime *rt = JS_GetRuntime(js);
|
||||
@@ -1352,17 +1354,17 @@ int main(int argc, char **argv)
|
||||
queue_cond = SDL_CreateCondition();
|
||||
actors_mutex = SDL_CreateMutex();
|
||||
|
||||
/* Create the initial actor from the main command line. */
|
||||
char **margv = malloc(sizeof(char *) * argc);
|
||||
for (int i = 0; i < argc; i++) margv[i] = strdup(argv[i]);
|
||||
create_actor(argc, margv);
|
||||
|
||||
const char *io_script = "scripts/core/io.js";
|
||||
char **io_argv = malloc(sizeof(char*)*2);
|
||||
io_argv[0] = strdup(argv[0]);
|
||||
io_argv[1] = strdup(io_script);
|
||||
io_actor = create_actor(2, io_argv);
|
||||
|
||||
/* Create the initial actor from the main command line. */
|
||||
char **margv = malloc(sizeof(char *) * argc);
|
||||
for (int i = 0; i < argc; i++) margv[i] = strdup(argv[i]);
|
||||
create_actor(argc, margv);
|
||||
|
||||
/* Start the thread that pumps ready actors, one per logical core. */
|
||||
for (int i = 0; i < cores; i++) {
|
||||
char threadname[128];
|
||||
@@ -1383,8 +1385,9 @@ int main(int argc, char **argv)
|
||||
if (event.type == queue_event)
|
||||
goto QUEUE;
|
||||
|
||||
WotaBuffer wb = event2wota(&event);
|
||||
send_message(io_actor->id, wb.data);
|
||||
// WotaBuffer wb = event2wota(&event);
|
||||
// send_message(io_actor->id, wb.data);
|
||||
continue;
|
||||
|
||||
QUEUE:
|
||||
SDL_LockMutex(queue_mutex);
|
||||
@@ -1411,3 +1414,8 @@ int actor_exists(char *id)
|
||||
else
|
||||
return 1;
|
||||
}
|
||||
|
||||
JSValue js_os_ioactor(JSContext *js, JSValue self, int argc, JSValue *argv)
|
||||
{
|
||||
return JS_NewString(js, io_actor->id);
|
||||
}
|
||||
|
||||
@@ -84,4 +84,6 @@ void set_actor_state(prosperon_rt *actor);
|
||||
|
||||
int prosperon_mount_core(void);
|
||||
|
||||
JSValue js_os_ioactor(JSContext *js, JSValue self, int argc, JSValue *argv);
|
||||
|
||||
#endif
|
||||
|
||||
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({
|
||||
title:`Prosperon [${prosperon.version}-${prosperon.revision}]`,
|
||||
width: 1280,
|
||||
@@ -19,13 +21,31 @@ prosperon.win = prosperon.engine_start({
|
||||
url: "https://prosperon.dev"
|
||||
})
|
||||
|
||||
var ren = prosperon.win.make_renderer("opengl")
|
||||
|
||||
function loop() {
|
||||
ren.clear()
|
||||
ren.fillrect({x:50,y:50,height:50,width:50})
|
||||
ren.present()
|
||||
$_.delay(loop, 1/60)
|
||||
}
|
||||
loop()
|
||||
|
||||
$_.delay($_.stop, 3)
|
||||
|
||||
$_.receiver(e => {
|
||||
console.log(json.encode(e))
|
||||
var os = use('os')
|
||||
var ioguy = {
|
||||
__ACTORDATA__: {
|
||||
id: os.ioactor()
|
||||
}
|
||||
}
|
||||
|
||||
$_.send(ioguy, {
|
||||
type: "subscribe",
|
||||
actor: $_
|
||||
})
|
||||
|
||||
$_.receiver(e => {
|
||||
if (e.type === 'quit')
|
||||
os.quit()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user