This commit is contained in:
2025-06-11 01:13:58 -05:00
parent 1c2b8228fe
commit b8ad8431f4
7 changed files with 496 additions and 363 deletions

View File

@@ -213,6 +213,7 @@ else
endif
deps += dependency('threads')
deps += dependency('mimalloc')
# Try to find system-installed chipmunk first
chipmunk_dep = dependency('chipmunk', static: true, required: false)

View File

@@ -7,19 +7,7 @@ A collection of 2D drawing functions that create drawing command lists.
These are pure functions that return plain JavaScript objects representing
drawing operations. No rendering or actor communication happens here.
`
// Default command list for convenience
var current_list = draw.list()
// Set the current list
draw.set_list = function(list) {
current_list = list
}
// Get current list
draw.get_list = function() {
return current_list
}
var current_list = []
// Clear current list
draw.clear = function() {
@@ -34,7 +22,7 @@ draw.get_commands = function() {
// Helper to add a command
function add_command(type, data) {
data.cmd = type
current_list.push(cmd)
current_list.push(data)
}
// Default geometry definitions
@@ -156,7 +144,7 @@ draw.slice9 = function slice9(image, rect = [0,0], slice = 0, info = slice9_info
})
}
draw.image = function image(image, rect, rotation = 0, anchor = [0,0], shear = [0,0], info = {}, material) {
draw.image = function image(image, rect, rotation, anchor, shear, info, material) {
if (!rect) throw Error('Need rectangle to render image.')
if (!image) throw Error('Need an image to render.')

View File

@@ -103,7 +103,7 @@ function worldToScreenRect(rect, camera) {
var camera = {
size: [640,320],//{width:500,height:500}, // pixel size the camera "sees", like its resolution
size: [640,480],//{width:500,height:500}, // pixel size the camera "sees", like its resolution
pos: [250,250],//{x:0,y:0}, // where it is
fov:50,
near_z:0,
@@ -145,7 +145,7 @@ function translate_draw_commands(commands) {
switch(cmd.cmd) {
case "draw_rect":
cmd.rect = worldToScreenRect(cmd.rect, camera,500, 500)
cmd.rect = worldToScreenRect(cmd.rect, camera)
// Handle rectangles with optional rounding and thickness
if (cmd.opt && cmd.opt.radius && cmd.opt.radius > 0) {
// Rounded rectangle
@@ -190,7 +190,7 @@ function translate_draw_commands(commands) {
case "draw_circle":
case "draw_ellipse":
cmd.pos = worldToScreenPoint(cmd.pos, camera, 500, 500)
cmd.pos = worldToScreenPoint(cmd.pos, camera)
// Rasterize ellipse to points or rects
var radii = cmd.radii || [cmd.radius, cmd.radius]
var raster_result = rasterize.ellipse(cmd.pos, radii, cmd.opt || {})
@@ -212,12 +212,12 @@ function translate_draw_commands(commands) {
case "draw_line":
renderer_commands.push({
op: "line",
data: {points: cmd.points.map(p => worldToScreenPoint(p, camera, 500, 500))}
data: {points: cmd.points.map(p => worldToScreenPoint(p, camera))}
})
break
case "draw_point":
cmd.pos = worldToScreenPoint(cmd.pos, camera, 500, 500)
cmd.pos = worldToScreenPoint(cmd.pos, camera)
renderer_commands.push({
op: "point",
data: {points: [cmd.pos]}
@@ -231,7 +231,7 @@ function translate_draw_commands(commands) {
cmd.rect.width ??= img.width
cmd.rect.height ??= img.height
cmd.rect = worldToScreenRect(cmd.rect, camera, 500, 500)
cmd.rect = worldToScreenRect(cmd.rect, camera)
renderer_commands.push({
op: "texture",
@@ -307,12 +307,12 @@ function create_batch(draw_cmds, done) {
batch.push(...translate_draw_commands(draw_cmds))
batch.push(
{op:'debugText', data:{pos:{x:10,y:10}, text:`Frame: ${frame_avg.toFixed(2)}ms`}},
{op:'set', prop:'drawColor', value:[1,1,1,1]},
{op:'debugText', data:{pos:{x:10,y:10}, text:`Fps: ${(1/frame_avg).toFixed(2)}`}},
{op:'present'}
)
send(video, {kind:'renderer', op:'batch', data:batch}, () => {
// update FPS
const now = time.number()
const dt = now - last_time
last_time = now
@@ -321,7 +321,7 @@ function create_batch(draw_cmds, done) {
if (frames.length > 60) frames.shift()
let sum = 0
for (let f of frames) sum += f
frame_avg = sum / frames.length * 1000
frame_avg = sum / frames.length
done(dt)
})
@@ -341,7 +341,7 @@ function start_pipeline() {
function render_step() {
// a) fire off the next update→draw immediately
const dt = time.number() - last_time
send(gameactor, {kind:'update', dt}, () =>
send(gameactor, {kind:'update', dt:1/60}, () =>
send(gameactor, {kind:'draw'}, cmds => pending_next = cmds)
)

View File

@@ -217,223 +217,246 @@ function handle_window(msg) {
}
}
// Renderer operation functions
var renderfuncs = {
destroy: function(msg) {
ren = undefined
return {success: true};
},
clear: function(msg) {
ren.clear();
return {success: true};
},
present: function(msg) {
ren.present();
return {success: true};
},
flush: function(msg) {
ren.flush();
return {success: true};
},
get: function(msg) {
var prop = msg.data ? msg.data.property : null;
if (!prop) return {error: "Missing property name"};
// Handle special getters that might return objects
if (prop === 'drawColor') {
var color = ren[prop];
if (color && typeof color === 'object') {
// Convert color object to array format [r,g,b,a]
return {data: [color.r || 0, color.g || 0, color.b || 0, color.a || 255]};
}
}
return {data: ren[prop]};
},
set: function(msg) {
var prop = msg.prop
var value = msg.value
if (!prop) return {error: "Missing property name"};
if (!value) return {error: "No value to set"}
// Validate property is settable
var readonly = ['window', 'name', 'outputSize', 'currentOutputSize', 'logicalPresentationRect', 'safeArea'];
if (readonly.indexOf(prop) !== -1) {
return {error: "Property '" + prop + "' is read-only"};
}
// Special handling for render target
if (prop === 'target' && value !== null && value !== undefined) {
var tex = resources.texture[value];
if (!tex) return {error: "Invalid texture id"};
value = tex;
}
ren[prop] = value;
return {success: true};
},
line: function(msg) {
if (!msg.data || !msg.data.points) return {error: "Missing points array"};
ren.line(msg.data.points);
return {success: true};
},
point: function(msg) {
if (!msg.data || !msg.data.points) return {error: "Missing points"};
ren.point(msg.data.points);
return {success: true};
},
rect: function(msg) {
if (!msg.data || !msg.data.rect) return {error: "Missing rect"};
ren.rect(msg.data.rect);
return {success: true};
},
fillRect: function(msg) {
if (!msg.data || !msg.data.rect) return {error: "Missing rect"};
ren.fillRect(msg.data.rect);
return {success: true};
},
rects: function(msg) {
if (!msg.data || !msg.data.rects) return {error: "Missing rects"};
ren.rects(msg.data.rects);
return {success: true};
},
lineTo: function(msg) {
if (!msg.data || !msg.data.a || !msg.data.b) return {error: "Missing points a and b"};
ren.lineTo(msg.data.a, msg.data.b);
return {success: true};
},
texture: function(msg) {
if (!msg.data) return {error: "Missing texture data"};
var tex_id = msg.data.texture_id;
if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"};
ren.texture(
resources.texture[tex_id],
msg.data.src,
msg.data.dst,
msg.data.angle || 0,
msg.data.anchor || {x:0.5, y:0.5}
);
return {success: true};
},
copyTexture: function(msg) {
if (!msg.data) return {error: "Missing texture data"};
var tex_id = msg.data.texture_id;
if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"};
var tex = resources.texture[tex_id];
// Use the texture method with normalized coordinates
ren.texture(
tex,
msg.data.src || {x:0, y:0, width:tex.width, height:tex.height},
msg.data.dest || {x:0, y:0, width:tex.width, height:tex.height},
0, // No rotation
{x:0, y:0} // Top-left anchor
);
return {success: true};
},
sprite: function(msg) {
if (!msg.data || !msg.data.sprite) return {error: "Missing sprite data"};
ren.sprite(msg.data.sprite);
return {success: true};
},
geometry: function(msg) {
if (!msg.data) return {error: "Missing geometry data"};
var tex_id = msg.data.texture_id;
var tex = tex_id ? resources.texture[tex_id] : null;
ren.geometry(tex, msg.data.geometry);
return {success: true};
},
debugText: function(msg) {
if (!msg.data || !msg.data.text) return {error: "Missing text"};
ren.debugText([msg.data.pos.x, msg.data.pos.y], msg.data.text);
return {success: true};
},
clipEnabled: function(msg) {
return {data: ren.clipEnabled()};
},
texture9Grid: function(msg) {
if (!msg.data) return {error: "Missing data"};
var tex_id = msg.data.texture_id;
if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"};
ren.texture9Grid(
resources.texture[tex_id],
msg.data.src,
msg.data.leftWidth,
msg.data.rightWidth,
msg.data.topHeight,
msg.data.bottomHeight,
msg.data.scale,
msg.data.dst
);
return {success: true};
},
textureTiled: function(msg) {
if (!msg.data) return {error: "Missing data"};
var tex_id = msg.data.texture_id;
if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"};
ren.textureTiled(
resources.texture[tex_id],
msg.data.src,
msg.data.scale || 1.0,
msg.data.dst
);
return {success: true};
},
readPixels: function(msg) {
var surf = ren.readPixels(msg.data ? msg.data.rect : null);
if (!surf) return {error: "Failed to read pixels"};
var surf_id = allocate_id();
resources.surface[surf_id] = surf;
return {id: surf_id};
},
loadTexture: function(msg) {
if (!msg.data) throw new Error("Missing data")
var tex;
// Direct surface data
var surf = new surface(msg.data)
if (!surf)
throw new Error("Must provide surface_id or surface data")
tex = ren.load_texture(surf);
if (!tex) throw new Error("Failed to load texture")
var tex_id = allocate_id();
resources.texture[tex_id] = tex;
return {
id: tex_id,
};
},
coordsFromWindow: function(msg) {
if (!msg.data || !msg.data.pos) return {error: "Missing pos"};
return {data: ren.coordsFromWindow(msg.data.pos)};
},
coordsToWindow: function(msg) {
if (!msg.data || !msg.data.pos) return {error: "Missing pos"};
return {data: ren.coordsToWindow(msg.data.pos)};
},
batch: function(msg) {
if (!msg.data || !Array.isArray(msg.data)) return {error: "Missing or invalid data array"};
for (var i = 0; i < msg.data.length; i++)
handle_renderer(msg.data[i]);
return {success:true};
}
};
// Renderer operations
function handle_renderer(msg) {
if (!ren) return{reason:'no renderer!'}
switch (msg.op) {
case 'destroy':
ren = undefined
return {success: true};
case 'clear':
ren.clear();
return {success: true};
case 'present':
ren.present();
return {success: true};
case 'flush':
ren.flush();
return {success: true};
case 'get':
var prop = msg.data ? msg.data.property : null;
if (!prop) return {error: "Missing property name"};
// Handle special getters that might return objects
if (prop === 'drawColor') {
var color = ren[prop];
if (color && typeof color === 'object') {
// Convert color object to array format [r,g,b,a]
return {data: [color.r || 0, color.g || 0, color.b || 0, color.a || 255]};
}
}
return {data: ren[prop]};
case 'set':
var prop = msg.prop
var value = msg.value
if (!prop) return {error: "Missing property name"};
if (!value) return {error: "No value to set"}
// Validate property is settable
var readonly = ['window', 'name', 'outputSize', 'currentOutputSize', 'logicalPresentationRect', 'safeArea'];
if (readonly.indexOf(prop) !== -1) {
return {error: "Property '" + prop + "' is read-only"};
}
// Special handling for render target
if (prop === 'target' && value !== null && value !== undefined) {
var tex = resources.texture[value];
if (!tex) return {error: "Invalid texture id"};
value = tex;
}
ren[prop] = value;
return {success: true};
case 'line':
if (!msg.data || !msg.data.points) return {error: "Missing points array"};
ren.line(msg.data.points);
return {success: true};
case 'point':
if (!msg.data || !msg.data.points) return {error: "Missing points"};
ren.point(msg.data.points);
return {success: true};
case 'rect':
if (!msg.data || !msg.data.rect) return {error: "Missing rect"};
ren.rect(msg.data.rect);
return {success: true};
case 'fillRect':
if (!msg.data || !msg.data.rect) return {error: "Missing rect"};
ren.fillRect(msg.data.rect);
return {success: true};
case 'rects':
if (!msg.data || !msg.data.rects) return {error: "Missing rects"};
ren.rects(msg.data.rects);
return {success: true};
case 'lineTo':
if (!msg.data || !msg.data.a || !msg.data.b) return {error: "Missing points a and b"};
ren.lineTo(msg.data.a, msg.data.b);
return {success: true};
case 'texture':
if (!msg.data) return {error: "Missing texture data"};
var tex_id = msg.data.texture_id;
if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"};
ren.texture(
resources.texture[tex_id],
msg.data.src,
msg.data.dst,
msg.data.angle || 0,
msg.data.anchor || {x:0.5, y:0.5}
);
return {success: true};
case 'copyTexture':
if (!msg.data) return {error: "Missing texture data"};
var tex_id = msg.data.texture_id;
if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"};
var tex = resources.texture[tex_id];
// Use the texture method with normalized coordinates
ren.texture(
tex,
msg.data.src || {x:0, y:0, width:tex.width, height:tex.height},
msg.data.dest || {x:0, y:0, width:tex.width, height:tex.height},
0, // No rotation
{x:0, y:0} // Top-left anchor
);
return {success: true};
case 'sprite':
if (!msg.data || !msg.data.sprite) return {error: "Missing sprite data"};
ren.sprite(msg.data.sprite);
return {success: true};
case 'geometry':
if (!msg.data) return {error: "Missing geometry data"};
var tex_id = msg.data.texture_id;
var tex = tex_id ? resources.texture[tex_id] : null;
ren.geometry(tex, msg.data.geometry);
return {success: true};
case 'debugText':
if (!msg.data || !msg.data.text) return {error: "Missing text"};
ren.debugText([msg.data.pos.x, msg.data.pos.y], msg.data.text);
return {success: true};
case 'clipEnabled':
return {data: ren.clipEnabled()};
case 'texture9Grid':
if (!msg.data) return {error: "Missing data"};
var tex_id = msg.data.texture_id;
if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"};
ren.texture9Grid(
resources.texture[tex_id],
msg.data.src,
msg.data.leftWidth,
msg.data.rightWidth,
msg.data.topHeight,
msg.data.bottomHeight,
msg.data.scale,
msg.data.dst
);
return {success: true};
case 'textureTiled':
if (!msg.data) return {error: "Missing data"};
var tex_id = msg.data.texture_id;
if (!tex_id || !resources.texture[tex_id]) return {error: "Invalid texture id"};
ren.textureTiled(
resources.texture[tex_id],
msg.data.src,
msg.data.scale || 1.0,
msg.data.dst
);
return {success: true};
case 'readPixels':
var surf = ren.readPixels(msg.data ? msg.data.rect : null);
if (!surf) return {error: "Failed to read pixels"};
var surf_id = allocate_id();
resources.surface[surf_id] = surf;
return {id: surf_id};
case 'loadTexture':
if (!msg.data) throw new Error("Missing data")
var tex;
// Direct surface data
var surf = new surface(msg.data)
if (!surf)
throw new Error("Must provide surface_id or surface data")
tex = ren.load_texture(surf);
if (!tex) throw new Error("Failed to load texture")
var tex_id = allocate_id();
resources.texture[tex_id] = tex;
return {
id: tex_id,
};
case 'flush':
ren.flush();
return {success: true};
case 'coordsFromWindow':
if (!msg.data || !msg.data.pos) return {error: "Missing pos"};
return {data: ren.coordsFromWindow(msg.data.pos)};
case 'coordsToWindow':
if (!msg.data || !msg.data.pos) return {error: "Missing pos"};
return {data: ren.coordsToWindow(msg.data.pos)};
case 'batch':
if (!msg.data || !Array.isArray(msg.data)) return {error: "Missing or invalid data array"};
var results = [];
for (var i = 0; i < msg.data.length; i++) {
var result = handle_renderer(msg.data[i]);
results.push(result);
}
return {success:true};
default:
return {error: "Unknown renderer operation: " + msg.op};
var func = renderfuncs[msg.op];
if (func) {
return func(msg);
} else {
return {error: "Unknown renderer operation: " + msg.op};
}
}

View File

@@ -41,6 +41,8 @@
#ifdef TRACY_ENABLE
#include <tracy/TracyC.h>
#endif
#if defined(__APPLE__)
#include <malloc/malloc.h>
#define MALLOC_OVERHEAD 0
@@ -51,10 +53,12 @@
#define _GNU_SOURCE
#include <malloc.h>
#define MALLOC_OVERHEAD 8
#else
#define MALLOC_OVERHEAD 0
#endif
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
#endif
int tracy_profiling_enabled = 0;
@@ -184,6 +188,7 @@ void actor_free(cell_rt *actor)
SDL_UnlockMutex(actor->msg_mutex);
SDL_DestroyMutex(actor->msg_mutex);
mi_heap_destroy(actor->heap);
free(actor);
SDL_LockMutex(actors_mutex);
@@ -197,10 +202,7 @@ void js_dofree(JSRuntime *rt, void *opaque, void *ptr)
js_free_rt(rt, ptr);
}
SDL_TLSID prosperon_id;
#ifdef TRACY_ENABLE
static size_t js_tracy_malloc_usable_size(const void *ptr)
static size_t js_mi_malloc_usable_size(const void *ptr)
{
#if defined(__APPLE__)
return malloc_size(ptr);
@@ -215,56 +217,86 @@ static size_t js_tracy_malloc_usable_size(const void *ptr)
#endif
}
static void *js_tracy_malloc(JSMallocState *s, size_t size)
{
void *js_mi_malloc(JSMallocState *s, size_t sz) {
void *ptr;
assert(size != 0);
if (unlikely(s->malloc_size + size > s->malloc_limit)) return NULL;
ptr = malloc(size);
assert(sz != 0);
if (unlikely(s->malloc_size + sz > s->malloc_limit)) return NULL;
cell_rt *actor = (cell_rt*)s->opaque;
ptr = mi_heap_malloc(actor->heap, sz);
if (!ptr) return NULL;
s->malloc_count++;
s->malloc_size += js_tracy_malloc_usable_size(ptr) + MALLOC_OVERHEAD;
TracyCAllocN(ptr, js_tracy_malloc_usable_size(ptr) + MALLOC_OVERHEAD, "quickjs");
s->malloc_size += js_mi_malloc_usable_size(ptr) + MALLOC_OVERHEAD;
#ifdef TRACY_ENABLE
if (tracy_profiling_enabled)
TracyCAllocN(ptr, js_mi_malloc_usable_size(ptr) + MALLOC_OVERHEAD, actor->name);
#endif
return ptr;
}
static void js_tracy_free(JSMallocState *s, void *ptr)
{
if (!ptr) return;
void js_mi_free(JSMallocState *s, void *p) {
if (!p) return;
s->malloc_count--;
s->malloc_size -= js_tracy_malloc_usable_size(ptr) + MALLOC_OVERHEAD;
TracyCFreeN(ptr, "quickjs");
free(ptr);
s->malloc_size -= js_mi_malloc_usable_size(p) + MALLOC_OVERHEAD;
#ifdef TRACY_ENABLE
if (tracy_profiling_enabled) {
cell_rt *actor = s->opaque;
TracyCFreeN(p, actor->name);
}
#endif
mi_free(p);
}
static void *js_tracy_realloc(JSMallocState *s, void *ptr, size_t size)
{
void *js_mi_realloc(JSMallocState *s, void *p, size_t sz) {
size_t old_size;
if (!ptr) return size ? js_tracy_malloc(s, size) : NULL;
old_size = js_tracy_malloc_usable_size(ptr);
if (!size) {
cell_rt *actor = (cell_rt*)s->opaque;
if (!p) return sz ? js_mi_malloc(s, sz) : NULL;
old_size = js_mi_malloc_usable_size(p);
if (!sz) {
s->malloc_count--;
s->malloc_size -= old_size + MALLOC_OVERHEAD;
TracyCFreeN(ptr, "quickjs");
free(ptr);
#ifdef TRACY_ENABLE
if (tracy_profiling_enabled)
TracyCFreeN(p, actor->name);
#endif
mi_free(p);
return NULL;
}
if (s->malloc_size + size - old_size > s->malloc_limit) return NULL;
TracyCFreeN(ptr, "quickjs");
ptr = realloc(ptr, size);
if (!ptr) return NULL;
s->malloc_size += js_tracy_malloc_usable_size(ptr) - old_size;
TracyCAllocN(ptr, js_tracy_malloc_usable_size(ptr) + MALLOC_OVERHEAD, "quickjs");
return ptr;
if (s->malloc_size + sz - old_size > s->malloc_limit) return NULL;
#ifdef TRACY_ENABLE
if (tracy_profiling_enabled)
TracyCFreeN(p, actor->name);
#endif
p = mi_heap_realloc(actor->heap, p, sz);
if (!p) return NULL;
s->malloc_size += js_mi_malloc_usable_size(p) - old_size;
#ifdef TRACY_ENABLE
if (tracy_profiling_enabled)
TracyCAllocN(p, js_mi_malloc_usable_size(p) + MALLOC_OVERHEAD, actor->name);
#endif
return p;
}
static const JSMallocFunctions tracy_malloc_funcs = {
js_tracy_malloc,
js_tracy_free,
js_tracy_realloc,
js_tracy_malloc_usable_size
static const JSMallocFunctions mimalloc_funcs = {
js_mi_malloc,
js_mi_free,
js_mi_realloc,
js_mi_malloc_usable_size
};
#endif
static void free_zip(void)
{
@@ -364,6 +396,7 @@ void actor_unneeded(cell_rt *actor, JSValue fn, double seconds)
cell_rt *create_actor(void *wota)
{
cell_rt *actor = calloc(sizeof(*actor), 1);
actor->heap = mi_heap_new();
actor->init_wota = wota;
actor->idx_buffer = JS_UNDEFINED;
actor->message_handle = JS_UNDEFINED;
@@ -701,14 +734,7 @@ void script_startup(cell_rt *prt)
{
JSRuntime *rt;
#ifdef TRACY_ENABLE
if (tracy_profiling_enabled)
rt = JS_NewRuntime2(&tracy_malloc_funcs, NULL);
else
rt = JS_NewRuntime();
#else
rt = JS_NewRuntime();
#endif
rt = JS_NewRuntime2(&mimalloc_funcs, prt);
JSContext *js = JS_NewContextRaw(rt);
JS_SetInterruptHandler(rt, actor_interrupt_cb, prt);

View File

@@ -7,6 +7,8 @@
#include "qjs_blob.h"
#include "blob.h"
#include <mimalloc.h>
/* Letter type for unified message queue */
typedef enum {
LETTER_BLOB, /* Blob message */
@@ -47,6 +49,7 @@ typedef struct {
typedef struct cell_rt {
JSContext *context;
mi_heap_t *heap;
JSValue idx_buffer;
JSValue on_exception;
JSValue message_handle;

View File

@@ -44,13 +44,9 @@ static JSClassDef js_matrix_class = {
.finalizer = js_matrix_finalizer,
};
// Forward declaration for exotic methods
static const JSClassExoticMethods js_array_exotic;
static JSClassDef js_array_class = {
"Array",
.finalizer = js_array_finalizer,
.exotic = &js_array_exotic,
};
static matrix_t *js2matrix(JSContext *ctx, JSValue v) {
@@ -422,6 +418,111 @@ static JSValue js_array_norm(JSContext *ctx, JSValueConst this_val, int argc, JS
return JS_NewFloat64(ctx, norm);
}
static JSValue js_array_multiply(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
if (argc < 1)
return JS_ThrowTypeError(ctx, "multiply requires an argument");
array_t *arr = js2array(ctx, this_val);
if (!arr)
return JS_EXCEPTION;
// Create result array
array_t *result = js_malloc(ctx, sizeof(array_t));
if (!result)
return JS_EXCEPTION;
result->size = arr->size;
result->data = js_malloc(ctx, sizeof(double) * result->size);
if (!result->data) {
js_free(ctx, result);
return JS_EXCEPTION;
}
// Copy original data
memcpy(result->data, arr->data, sizeof(double) * arr->size);
// Check if multiplying by scalar
if (JS_IsNumber(argv[0])) {
double scalar;
if (JS_ToFloat64(ctx, &scalar, argv[0]))
return JS_EXCEPTION;
// Multiply each element by scalar
if (result->size <= 4)
for (int i = 0; i < result->size; i++)
result->data[i] *= scalar;
else
cblas_dscal(result->size, scalar, result->data, 1);
} else {
js_free(ctx, result->data);
js_free(ctx, result);
return JS_ThrowTypeError(ctx, "multiply requires a number argument");
}
JSValue obj = JS_NewObjectClass(ctx, js_array_class_id);
JS_SetOpaque(obj, result);
return obj;
}
static JSValue js_array_add(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
if (argc < 1)
return JS_ThrowTypeError(ctx, "add requires an argument");
array_t *arr = js2array(ctx, this_val);
if (!arr)
return JS_EXCEPTION;
// Create result array
array_t *result = js_malloc(ctx, sizeof(array_t));
if (!result)
return JS_EXCEPTION;
result->size = arr->size;
result->data = js_malloc(ctx, sizeof(double) * result->size);
if (!result->data) {
js_free(ctx, result);
return JS_EXCEPTION;
}
// Copy original data
memcpy(result->data, arr->data, sizeof(double) * arr->size);
// Check if adding scalar
if (JS_IsNumber(argv[0])) {
double scalar;
if (JS_ToFloat64(ctx, &scalar, argv[0]))
return JS_EXCEPTION;
// Add scalar to each element
for (int i = 0; i < result->size; i++)
result->data[i] += scalar;
} else {
// Check if adding another array
array_t *other = js2array(ctx, argv[0]);
if (other) {
if (arr->size != other->size) {
js_free(ctx, result->data);
js_free(ctx, result);
return JS_ThrowTypeError(ctx, "Arrays must have the same size for element-wise addition");
}
if (arr->size <= 4) {
for (int i = 0; i < arr->size; i++)
result->data[i] += other->data[i];
} else
cblas_daxpy(result->size, 1.0, other->data, 1, result->data, 1);
} else {
js_free(ctx, result->data);
js_free(ctx, result);
return JS_ThrowTypeError(ctx, "add requires a number or Array argument");
}
}
JSValue obj = JS_NewObjectClass(ctx, js_array_class_id);
JS_SetOpaque(obj, result);
return obj;
}
static JSValue js_array_slice(JSContext *js, JSValueConst self, int argc, JSValueConst *argv)
{
array_t *arr = js2array(js, self);
@@ -477,87 +578,75 @@ static JSValue js_array_slice(JSContext *js, JSValueConst self, int argc, JSValu
return jsarr;
}
static int js_array_get_own_property(JSContext *ctx, JSPropertyDescriptor *desc,
JSValueConst obj, JSAtom prop)
{
array_t *arr = JS_GetOpaque(obj, js_array_class_id);
if(!arr) return FALSE;
if(JS_AtomIsNumericIndex(ctx, prop) > 0) {
JSValue key = JS_AtomToValue(ctx, prop);
uint32_t idx;
JS_ToUint32(ctx, &idx, key);
JS_FreeValue(ctx, key);
if(idx < arr->size) {
if(desc) {
desc->flags = JS_PROP_C_W_E; /* configurable, writable, enumerable */
desc->value = JS_NewFloat64(ctx, arr->data[idx]);
desc->getter = JS_UNDEFINED;
desc->setter = JS_UNDEFINED;
}
return TRUE;
static JSValue js_array_multiplyf(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
if (argc < 1)
return JS_ThrowTypeError(ctx, "multiplyf requires an argument");
array_t *arr = js2array(ctx, this_val);
if (!arr)
return JS_EXCEPTION;
// Check if multiplying by scalar
if (JS_IsNumber(argv[0])) {
double scalar;
if (JS_ToFloat64(ctx, &scalar, argv[0]))
return JS_EXCEPTION;
// Multiply each element by scalar in-place
if (arr->size <= 4) {
for (int i = 0; i < arr->size; i++)
arr->data[i] *= scalar;
} else {
cblas_dscal(arr->size, scalar, arr->data, 1);
}
return JS_DupValue(ctx, this_val);
} else {
return JS_ThrowTypeError(ctx, "multiplyf requires a number argument");
}
return FALSE;
}
int ret;
const char *name = JS_AtomToCString(ctx, prop);
if (name && strcmp(name, "length") == 0) {
if(desc) {
/* length is read-only, non-enumerable */
desc->flags = JS_PROP_CONFIGURABLE;
desc->value = JS_NewInt32(ctx, arr->size);
desc->getter = JS_UNDEFINED;
desc->setter = JS_UNDEFINED;
}
ret = TRUE;
}
ret = FALSE;
JS_FreeCString(ctx, name);
return ret;
}
#include "quickjs-atom.h"
static int js_array_define_own_property(JSContext *ctx, JSValueConst this_obj,
JSAtom prop, JSValueConst val,
JSValueConst getter, JSValueConst setter,
int flags)
{
array_t *arr = JS_GetOpaque(this_obj, js_array_class_id);
if(!arr) return FALSE;
if(JS_AtomIsNumericIndex(ctx, prop) > 0) {
JSValue key = JS_AtomToValue(ctx, prop);
uint32_t idx;
JS_ToUint32(ctx, &idx, key);
JS_FreeValue(ctx, key);
if(idx < arr->size) {
double d;
if(JS_ToFloat64(ctx, &d, val) == 0) {
arr->data[idx] = d;
return TRUE;
}
return FALSE;
static JSValue js_array_addf(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
if (argc < 1)
return JS_ThrowTypeError(ctx, "addf requires an argument");
array_t *arr = js2array(ctx, this_val);
if (!arr)
return JS_EXCEPTION;
// Check if adding scalar
if (JS_IsNumber(argv[0])) {
double scalar;
if (JS_ToFloat64(ctx, &scalar, argv[0]))
return JS_EXCEPTION;
// Add scalar to each element in-place
for (int i = 0; i < arr->size; i++)
arr->data[i] += scalar;
} else {
// Check if adding another array
array_t *other = js2array(ctx, argv[0]);
if (other) {
if (arr->size != other->size) {
return JS_ThrowTypeError(ctx, "Arrays must have the same size for element-wise addition");
}
// Add arrays element-wise in-place
if (arr->size <= 4) {
for (int i = 0; i < arr->size; i++)
arr->data[i] += other->data[i];
} else {
cblas_daxpy(arr->size, 1.0, other->data, 1, arr->data, 1);
}
} else {
return JS_ThrowTypeError(ctx, "addf requires a number or Array argument");
}
}
return FALSE;
}
/* reject everything else (including “length”) */
return FALSE;
return JS_DupValue(ctx, this_val);
}
// Set up the exotic methods structure
static const JSClassExoticMethods js_array_exotic = {
.get_own_property = js_array_get_own_property,
.define_own_property = js_array_define_own_property,
// Other methods can be NULL for now
};
static const JSCFunctionListEntry js_matrix_proto_funcs[] = {
JS_CFUNC_DEF("inverse", 0, js_matrix_inverse),
@@ -568,12 +657,15 @@ static const JSCFunctionListEntry js_matrix_proto_funcs[] = {
static const JSCFunctionListEntry js_array_proto_funcs[] = {
JS_CFUNC_DEF("dot", 1, js_array_dot),
JS_CFUNC_DEF("norm", 0, js_array_norm),
JS_CFUNC_DEF("multiply", 1, js_array_multiply),
JS_CFUNC_DEF("add", 1, js_array_add),
JS_CFUNC_DEF("multiplyf", 1, js_array_multiplyf),
JS_CFUNC_DEF("addf", 1, js_array_addf),
JS_CFUNC_DEF("toArray", 0, js_array_to_array),
JS_CFUNC_DEF("toJSON", 0, js_array_to_array), /* ← for JSON.stringify */
JS_CFUNC_DEF("slice", 2, js_array_slice),
};
JSValue js_num_use(JSContext *ctx) {
// Register Matrix class
JS_NewClassID(&js_matrix_class_id);