animations

This commit is contained in:
2025-07-15 15:06:06 -05:00
5 changed files with 294 additions and 155 deletions

View File

@@ -201,7 +201,7 @@ function translate_draw_commands(commands) {
renderer_commands.push({ renderer_commands.push({
op: "texture", op: "texture",
data: { data: {
texture_id: gpu.id, texture_id: gpu,
dst: cmd.rect, dst: cmd.rect,
src: img.rect src: img.rect
} }
@@ -233,7 +233,7 @@ function translate_draw_commands(commands) {
renderer_commands.push({ renderer_commands.push({
op: "texture9Grid", op: "texture9Grid",
data: { data: {
texture_id: gpu.id, texture_id: gpu,
src: img.rect, src: img.rect,
leftWidth: cmd.slice, leftWidth: cmd.slice,
rightWidth: cmd.slice, rightWidth: cmd.slice,
@@ -274,7 +274,7 @@ function translate_draw_commands(commands) {
num_vertices: geom.num_vertices, num_vertices: geom.num_vertices,
num_indices: geom.num_indices, num_indices: geom.num_indices,
size_indices: geom.size_indices, size_indices: geom.size_indices,
texture_id: gpu.id texture_id: gpu
} }
renderer_commands.push({ renderer_commands.push({
@@ -285,9 +285,17 @@ function translate_draw_commands(commands) {
break break
case "geometry": case "geometry":
var img = graphics.texture(cmd.image) var texture_id
var gpu = img.gpu if (cmd.texture_id) {
if (!gpu) break // Use the provided texture ID directly
texture_id = cmd.texture_id
} else {
// Fall back to looking up by image path
var img = graphics.texture(cmd.image)
var gpu = img.gpu
if (!gpu) break
texture_id = gpu
}
// Transform geometry through camera and send to renderer // Transform geometry through camera and send to renderer
var geom = cmd.geometry var geom = cmd.geometry
@@ -308,7 +316,7 @@ function translate_draw_commands(commands) {
num_vertices: geom.num_vertices, num_vertices: geom.num_vertices,
num_indices: geom.num_indices, num_indices: geom.num_indices,
size_indices: geom.size_indices, size_indices: geom.size_indices,
texture_id: gpu.id texture_id: texture_id
} }
renderer_commands.push({ renderer_commands.push({

View File

@@ -7,7 +7,7 @@ function tilemap()
this.offset_y = 0; this.offset_y = 0;
this.size_x = 32; this.size_x = 32;
this.size_y = 32; this.size_y = 32;
this._geometry_cache = null; this._geometry_cache = {}; // Cache actual geometry data by texture
this._dirty = true; this._dirty = true;
return this; return this;
} }
@@ -65,6 +65,12 @@ tilemap.prototype =
// Ensure array exists up to x // Ensure array exists up to x
while (this.tiles.length <= x) this.tiles.push([]); while (this.tiles.length <= x) this.tiles.push([]);
// Convert string to image object if needed
if (image && typeof image == 'string') {
var graphics = use('graphics');
image = graphics.texture(image);
}
// Set the value // Set the value
this.tiles[x][y] = image; this.tiles[x][y] = image;
@@ -76,8 +82,10 @@ tilemap.prototype =
_build_geometry_cache(pos = {x: 0, y: 0}) { _build_geometry_cache(pos = {x: 0, y: 0}) {
var geometry = use('geometry'); var geometry = use('geometry');
// Group tiles by texture // Group tiles by texture (using a unique key per image object)
var textureGroups = {}; var textureGroups = {};
var imageToKey = new Map(); // Map image objects to unique keys
var keyCounter = 0;
// Collect all tiles and their positions // Collect all tiles and their positions
for (var x = 0; x < this.tiles.length; x++) { for (var x = 0; x < this.tiles.length; x++) {
@@ -85,10 +93,19 @@ tilemap.prototype =
for (var y = 0; y < this.tiles[x].length; y++) { for (var y = 0; y < this.tiles[x].length; y++) {
var tile = this.tiles[x][y]; var tile = this.tiles[x][y];
if (tile) { if (tile) {
var textureKey = tile; // tile should already be an image object
// Create a unique key for each distinct image object
if (!imageToKey.has(tile)) {
var key = `texture_${keyCounter++}`;
imageToKey.set(tile, key);
log.console(`New texture key: ${key} for tile ${tile}`)
}
var textureKey = imageToKey.get(tile);
if (!textureGroups[textureKey]) { if (!textureGroups[textureKey]) {
textureGroups[textureKey] = { textureGroups[textureKey] = {
tiles: [], tiles: [],
image: tile, // Store the image object
offset_x: this.offset_x, offset_x: this.offset_x,
offset_y: this.offset_y, offset_y: this.offset_y,
size_x: this.size_x, size_x: this.size_x,
@@ -104,8 +121,8 @@ tilemap.prototype =
} }
} }
// Generate geometry for each texture group // Generate and cache geometry for each texture group
var geometryCommands = []; this._geometry_cache = {};
for (var textureKey in textureGroups) { for (var textureKey in textureGroups) {
var group = textureGroups[textureKey]; var group = textureGroups[textureKey];
if (group.tiles.length == 0) continue; if (group.tiles.length == 0) continue;
@@ -129,29 +146,38 @@ tilemap.prototype =
tempMap.tiles[arrayX][arrayY] = image; tempMap.tiles[arrayX][arrayY] = image;
}); });
// Generate geometry for this group // Generate and cache geometry for this group
var geom = geometry.tilemap_to_data(tempMap); var geom = geometry.tilemap_to_data(tempMap);
this._geometry_cache[textureKey] = {
geometryCommands.push({
cmd: "geometry",
geometry: geom, geometry: geom,
image: textureKey image: group.image
}); };
} }
this._geometry_cache = geometryCommands;
this._dirty = false; this._dirty = false;
}, },
draw(pos = {x: 0, y: 0}) { draw(pos = {x: 0, y: 0}) {
// Rebuild cache if dirty or position changed // Rebuild cache if dirty or position changed
if (this._dirty || !this._geometry_cache || this._last_pos?.x != pos.x || this._last_pos?.y != pos.y) { if (this._dirty || Object.keys(this._geometry_cache).length == 0 || this._last_pos?.x != pos.x || this._last_pos?.y != pos.y) {
this._build_geometry_cache(pos); this._build_geometry_cache(pos);
this._last_pos = {x: pos.x, y: pos.y}; this._last_pos = {x: pos.x, y: pos.y};
} }
// Return cached geometry commands // Generate commands from cached geometry, pulling texture_id dynamically
return this._geometry_cache; var commands = [];
var i = 0
for (var textureKey in this._geometry_cache) {
var cached = this._geometry_cache[textureKey];
commands.push({
cmd: "geometry",
geometry: cached.geometry,
image: cached.image,
texture_id: cached.image.gpu // Pull GPU ID dynamically on each draw
});
}
return commands;
}, },
} }

View File

@@ -19,94 +19,97 @@ var LASTUSE = Symbol()
var LOADING = Symbol() var LOADING = Symbol()
var cache = {} var cache = {}
var pending_gpu_loads = []
graphics.setup = function(renderer) graphics.setup = function(renderer)
{ {
renderer_actor = renderer renderer_actor = renderer
// Process any pending GPU loads
if (renderer_actor && pending_gpu_loads.length > 0) {
log.console(`Processing ${pending_gpu_loads.length} pending GPU loads`)
for (var img of pending_gpu_loads) {
img.loadGPU()
}
pending_gpu_loads = []
}
// Also process any cached images that need GPU loading
if (renderer_actor) {
for (var key in cache) {
var img = cache[key]
if (img instanceof graphics.Image && img.cpu && img.gpu == 0) {
img.loadGPU()
}
}
}
} }
// Image constructor function // Image constructor function
graphics.Image = function(surfaceData) { graphics.Image = function(surfaceData) {
// Initialize private properties // Initialize properties
this[CPU] = surfaceData || null; this.cpu = surfaceData || null;
this[GPU] = null; this.gpu = 0;
this.texture = 0;
this.surface = this.cpu;
this.width = surfaceData?.width || 0;
this.height = surfaceData?.height || 0;
this.rect = {x:0, y:0, width:this.width, height:this.height};
this[LOADING] = false; this[LOADING] = false;
this[LASTUSE] = time.number(); this[LASTUSE] = time.number();
this.rect = {x:0, y:0, width:surfaceData.width, height:surfaceData.height};
// Load GPU texture if renderer is available, otherwise queue it
if (renderer_actor && this.cpu) {
this.loadGPU();
} else if (this.cpu) {
// Queue for later GPU loading when renderer is available
pending_gpu_loads.push(this)
}
} }
// Define getters and methods on the prototype graphics.Image.prototype.loadGPU = function() {
Object.defineProperties(graphics.Image.prototype, { if (!this[LOADING] && renderer_actor && this.cpu) {
gpu: { this[LOADING] = true;
get: function() { var self = this;
if (!this[GPU] && !this[LOADING] && renderer_actor) {
this[LOADING] = true; send(renderer_actor, {
var self = this; kind: "renderer",
op: "loadTexture",
// Send message to load texture data: this.cpu
send(renderer_actor, { }, function(response) {
kind: "renderer", if (response.error) {
op: "loadTexture", log.error("Failed to load texture:")
data: this[CPU] log.error(response.error)
}, function(response) { self[LOADING] = false;
if (response.error) { } else {
log.error("Failed to load texture:") // Store the full response as texture (has width/height)
log.error(response.error) self.texture = response;
self[LOADING] = false; // Store just the ID as gpu
} else { self.gpu = response.id || response;
self[GPU] = response; decorate_rect_px(self);
decorate_rect_px(self); self[LOADING] = false;
self[LOADING] = false;
}
});
} }
});
return this[GPU]
}
},
texture: {
get: function() { return this.gpu }
},
cpu: {
get: function() {
return this[CPU]
}
},
surface: {
get: function() { return this.cpu }
},
width: {
get: function() {
return this[CPU]?.width || 0
}
},
height: {
get: function() {
return this[CPU]?.height || 0
}
} }
}); }
// Add methods to prototype // Add methods to prototype
graphics.Image.prototype.unload_gpu = function() { graphics.Image.prototype.unload_gpu = function() {
this[GPU] = null this.gpu = 0;
this.texture = 0;
} }
graphics.Image.prototype.unload_cpu = function() { graphics.Image.prototype.unload_cpu = function() {
this[CPU] = null this.cpu = null;
this.surface = null;
} }
function calc_image_size(img) { function calc_image_size(img) {
if (!img.rect) return if (!img.rect) return
if (img.texture) { if (img.texture) {
return [img.texture.width * img.rect.width, img.texture.height * img.rect.height] return [img.texture.width * img.rect.width, img.texture.height * img.rect.height]
} else if (img[CPU]) { } else if (img.cpu) {
return [img[CPU].width * img.rect.width, img[CPU].height * img.rect.height] return [img.cpu.width * img.rect.width, img.cpu.height * img.rect.height]
} }
return [0, 0] return [0, 0]
} }
@@ -119,9 +122,9 @@ function decorate_rect_px(img) {
if (img.texture) { if (img.texture) {
width = img.texture.width; width = img.texture.width;
height = img.texture.height; height = img.texture.height;
} else if (img[CPU]) { } else if (img.cpu) {
width = img[CPU].width; width = img.cpu.width;
height = img[CPU].height; height = img.cpu.height;
} else { } else {
return; return;
} }
@@ -148,11 +151,15 @@ function wrapSurface(surf, maybeRect){
return h; return h;
} }
function wrapFrames(arr){ /* [{surface,time,rect}, …] → [{image,time}] */ function wrapFrames(arr){ /* [{surface,time,rect}, …] → [{image,time}] */
return arr.map(f => ({ return arr.map(f => {
image : wrapSurface(f.surface || f), /* accept bare surface too */ // Handle both surface objects and objects with surface property
time: f.time, var surf = f.surface || f;
rect: f.rect /* keep for reference */ return {
})); image: wrapSurface(surf),
time: f.time || 0,
rect: f.rect /* keep for reference */
}
});
} }
function makeAnim(frames, loop=true){ function makeAnim(frames, loop=true){
return { frames, loop } return { frames, loop }
@@ -161,13 +168,10 @@ function makeAnim(frames, loop=true){
function decode_image(bytes, ext) function decode_image(bytes, ext)
{ {
switch(ext) { switch(ext) {
case 'gif': case 'gif': return graphics.make_gif(bytes) // returns array of surfaces
var g = graphics.make_gif(bytes)
if (g.frames) return g.frames[0]
return g
case 'ase': case 'ase':
case 'aseprite': return graphics.make_aseprite(bytes) case 'aseprite': return graphics.make_aseprite(bytes)
default: return {surface:graphics.make_texture(bytes)} default: return graphics.make_texture(bytes) // returns single surface
} }
} }
@@ -175,32 +179,47 @@ function create_image(path){
try{ try{
def bytes = io.slurpbytes(path); def bytes = io.slurpbytes(path);
let raw = decode_image(bytes, path.ext()); let raw = decode_image(bytes, path.ext());
/* ── Case A: static image ─────────────────────────────────── */ /* ── Case A: single surface (from make_texture) ────────────── */
if(raw.surface) { if(raw && raw.width && raw.pixels && !Array.isArray(raw)) {
var gg = new graphics.Image(raw.surface) return new graphics.Image(raw)
return gg
} }
/* ── Case B: GIF helpers returned array [surf, …] ─────────── */ /* ── Case B: array of surfaces (from make_gif) ────────────── */
if(Array.isArray(raw)) if(Array.isArray(raw)) {
return makeAnim( wrapFrames(raw), true ); // Single frame GIF returns array with one surface
if(raw.length == 1 && !raw[0].time) {
/* ── Case C: GIF helpers returned {frames,loop} ───────────── */ return new graphics.Image(raw[0])
if(raw.frames && Array.isArray(raw.frames)) }
return makeAnim( wrapFrames(raw.frames), !!raw.loop ); // Multiple frames - create animation
return makeAnim(wrapFrames(raw), true);
/* ── Case D: ASE helpers returned { animName:{frames,loop}, … } ── */ }
def anims = {};
for(def [name, anim] of Object.entries(raw)){ /* ── Case C: ASE helpers returned { animName:{frames,loop}, … } or single frame ── */
if(anim && Array.isArray(anim.frames)) if(typeof raw == 'object' && !raw.width) {
anims[name] = makeAnim( wrapFrames(anim.frames), !!anim.loop ); // Check if it's a single surface from ASE (single frame, no tags)
else if(anim && anim.surface) /* ase with flat surface */ if(raw.surface) {
anims[name] = makeAnim( return new graphics.Image(raw.surface)
[{image:make_handle(anim.surface),time:0}], true ); }
// Check if it's an untagged animation (multiple frames, no tags)
// This happens when ASE has no tags but multiple frames
if(raw.frames && Array.isArray(raw.frames) && raw.loop != null) {
log.console(`[Graphics] Loading untagged ASE animation with ${raw.frames.length} frames`)
return makeAnim(wrapFrames(raw.frames), !!raw.loop);
}
// Multiple named animations from ASE (with tags)
def anims = {};
for(def [name, anim] of Object.entries(raw)){
if(anim && Array.isArray(anim.frames))
anims[name] = makeAnim(wrapFrames(anim.frames), !!anim.loop);
else if(anim && anim.surface)
anims[name] = new graphics.Image(anim.surface);
}
if(Object.keys(anims).length) return anims;
} }
if(Object.keys(anims).length) return anims;
throw new Error('Unsupported image structure from decoder'); throw new Error('Unsupported image structure from decoder');
@@ -216,9 +235,9 @@ image.dimensions = function() {
if (this.texture) { if (this.texture) {
width = this.texture.width; width = this.texture.width;
height = this.texture.height; height = this.texture.height;
} else if (this[CPU]) { } else if (this.cpu) {
width = this[CPU].width; width = this.cpu.width;
height = this[CPU].height; height = this.cpu.height;
} }
return [width, height].scale([this.rect[2], this.rect[3]]) return [width, height].scale([this.rect[2], this.rect[3]])
} }
@@ -287,9 +306,9 @@ graphics.texture = function texture(path) {
if (!ipath) if (!ipath)
throw new Error(`unknown image ${id}`) throw new Error(`unknown image ${id}`)
var image = create_image(ipath) var result = create_image(ipath)
cache[id] = image cache[id] = result
return image return result // Can be Image, animation, or collection of animations
} }
graphics.texture[cell.DOC] = ` graphics.texture[cell.DOC] = `
:param path: A string path to an image file or an already-loaded image object. :param path: A string path to an image file or an already-loaded image object.
@@ -330,24 +349,28 @@ graphics.tex_hotreload = function tex_hotreload(file) {
var oldimg = cache[basename] var oldimg = cache[basename]
// Preserve the GPU texture ID if it exists // Preserve the GPU texture ID if it exists
var oldGPU = oldimg[GPU] var oldGPU = oldimg.gpu
// Update the CPU surface data // Update the CPU surface data
oldimg[CPU] = img[CPU] oldimg.cpu = img.cpu
oldimg.surface = img.cpu
// Clear GPU texture to force reload // Clear GPU texture to force reload
oldimg[GPU] = null oldimg.gpu = 0
oldimg.texture = 0
oldimg[LOADING] = false oldimg[LOADING] = false
// Update dimensions // Update dimensions
if (img[CPU]) { if (img.cpu) {
oldimg.rect = {x:0, y:0, width:img[CPU].width, height:img[CPU].height} oldimg.width = img.cpu.width
oldimg.height = img.cpu.height
oldimg.rect = {x:0, y:0, width:img.cpu.width, height:img.cpu.height}
decorate_rect_px(oldimg) decorate_rect_px(oldimg)
} }
// If the texture was on GPU, trigger reload // If the texture was on GPU, trigger reload
if (oldGPU && renderer_actor) { if (oldGPU && renderer_actor) {
oldimg.gpu // This getter will trigger the reload oldimg.loadGPU()
} }
} }
graphics.tex_hotreload[cell.DOC] = ` graphics.tex_hotreload[cell.DOC] = `

View File

@@ -1078,28 +1078,16 @@ JSC_CCALL(os_make_gif,
int height; int height;
void *pixels = stbi_load_gif_from_memory(raw, rawlen, &delays, &width, &height, &frames, &n, 4); void *pixels = stbi_load_gif_from_memory(raw, rawlen, &delays, &width, &height, &frames, &n, 4);
JSValue gif = JS_NewObject(js); if (!pixels) {
ret = gif; return JS_ThrowReferenceError(js, "Failed to decode GIF: %s", stbi_failure_reason());
if (frames == 1) {
// still image, so return surface data object
JSValue surfData = JS_NewObject(js);
JS_SetPropertyStr(js, surfData, "width", JS_NewInt32(js, width));
JS_SetPropertyStr(js, surfData, "height", JS_NewInt32(js, height));
JS_SetPropertyStr(js, surfData, "format", JS_NewString(js, "rgba32"));
JS_SetPropertyStr(js, surfData, "pitch", JS_NewInt32(js, width*4));
JS_SetPropertyStr(js, surfData, "pixels", js_new_blob_stoned_copy(js, pixels, width*height*4));
JS_SetPropertyStr(js, gif, "surface", surfData);
return gif;
} }
JSValue delay_arr = JS_NewArray(js); // Always return an array of surfaces, even for single frame
JSValue surface_array = JS_NewArray(js);
ret = surface_array;
for (int i = 0; i < frames; i++) { for (int i = 0; i < frames; i++) {
JSValue frame = JS_NewObject(js); // Create surface data object
JS_SetPropertyStr(js, frame, "time", number2js(js,(float)delays[i]/1000.0));
// Create surface data object instead of SDL_Surface
JSValue surfData = JS_NewObject(js); JSValue surfData = JS_NewObject(js);
JS_SetPropertyStr(js, surfData, "width", JS_NewInt32(js, width)); JS_SetPropertyStr(js, surfData, "width", JS_NewInt32(js, width));
JS_SetPropertyStr(js, surfData, "height", JS_NewInt32(js, height)); JS_SetPropertyStr(js, surfData, "height", JS_NewInt32(js, height));
@@ -1109,15 +1097,17 @@ JSC_CCALL(os_make_gif,
void *frame_pixels = (unsigned char*)pixels+(width*height*4*i); void *frame_pixels = (unsigned char*)pixels+(width*height*4*i);
JS_SetPropertyStr(js, surfData, "pixels", js_new_blob_stoned_copy(js, frame_pixels, width*height*4)); JS_SetPropertyStr(js, surfData, "pixels", js_new_blob_stoned_copy(js, frame_pixels, width*height*4));
JS_SetPropertyStr(js, frame, "surface", surfData); // Add time property for animation frames
JS_SetPropertyUint32(js, delay_arr, i, frame); if (frames > 1 && delays) {
JS_SetPropertyStr(js, surfData, "time", number2js(js,(float)delays[i]/1000.0));
}
JS_SetPropertyUint32(js, surface_array, i, surfData);
} }
JS_SetPropertyStr(js, gif, "frames", delay_arr);
CLEANUP: CLEANUP:
free(delays); if (delays) free(delays);
free(pixels); if (pixels) free(pixels);
) )
JSValue aseframe2js(JSContext *js, ase_frame_t aframe) JSValue aseframe2js(JSContext *js, ase_frame_t aframe)
@@ -1153,6 +1143,19 @@ JSC_CCALL(os_make_aseprite,
JSValue obj = aseframe2js(js,ase->frames[0]); JSValue obj = aseframe2js(js,ase->frames[0]);
cute_aseprite_free(ase); cute_aseprite_free(ase);
return obj; return obj;
} else {
// Multiple frames but no tags - create a simple animation
JSValue obj = JS_NewObject(js);
JSValue frames = JS_NewArray(js);
for (int f = 0; f < ase->frame_count; f++) {
JSValue frame = aseframe2js(js,ase->frames[f]);
JS_SetPropertyUint32(js, frames, f, frame);
}
JS_SetPropertyStr(js, obj, "frames", frames);
JS_SetPropertyStr(js, obj, "loop", JS_NewBool(js, true));
ret = obj;
cute_aseprite_free(ase);
return ret;
} }
} }

View File

@@ -1242,6 +1242,82 @@ JSC_CCALL(geometry_transform_xy_blob,
ret = transformed_blob; ret = transformed_blob;
) )
// RENDERITEM SYSTEM
typedef struct {
int layer;
float y;
JSValue val;
} RenderItem;
#define MAX_RENDER_ITEMS 64000
static RenderItem render_items[MAX_RENDER_ITEMS];
static int render_item_count = 0;
JSC_CCALL(geometry_renderitem_push,
if (argc < 3) {
return JS_ThrowTypeError(js, "renderitem_push requires 3 arguments: layer, y, val");
}
if (render_item_count >= MAX_RENDER_ITEMS) {
return JS_ThrowTypeError(js, "Maximum render items exceeded");
}
int layer;
double y;
if (JS_ToInt32(js, &layer, argv[0]) < 0) {
return JS_ThrowTypeError(js, "layer must be a number");
}
if (JS_ToFloat64(js, &y, argv[1]) < 0) {
return JS_ThrowTypeError(js, "y must be a number");
}
render_items[render_item_count].layer = layer;
render_items[render_item_count].y = (float)y;
render_items[render_item_count].val = JS_DupValue(js, argv[2]);
render_item_count++;
return JS_NULL;
)
static int compare_render_items(const void *a, const void *b)
{
const RenderItem *item_a = (const RenderItem *)a;
const RenderItem *item_b = (const RenderItem *)b;
if (item_a->layer != item_b->layer) {
return item_a->layer - item_b->layer;
}
// if (item_a->y != item_b->y) {
// return (item_b->y > item_a->y) ? 1 : -1;
// }
return 0;
}
JSC_CCALL(geometry_renderitem_sort,
qsort(render_items, render_item_count, sizeof(RenderItem), compare_render_items);
JSValue result = JS_NewArray(js);
for (int i = 0; i < render_item_count; i++) {
JS_SetPropertyUint32(js, result, i, JS_DupValue(js, render_items[i].val));
}
return result;
)
JSC_CCALL(geometry_renderitem_clear,
for (int i = 0; i < render_item_count; i++) {
JS_FreeValue(js, render_items[i].val);
}
render_item_count = 0;
return JS_NULL;
)
static const JSCFunctionListEntry js_geometry_funcs[] = { static const JSCFunctionListEntry js_geometry_funcs[] = {
MIST_FUNC_DEF(geometry, rect_intersection, 2), MIST_FUNC_DEF(geometry, rect_intersection, 2),
MIST_FUNC_DEF(geometry, rect_intersects, 2), MIST_FUNC_DEF(geometry, rect_intersects, 2),
@@ -1260,6 +1336,9 @@ static const JSCFunctionListEntry js_geometry_funcs[] = {
MIST_FUNC_DEF(gpu, slice9, 3), MIST_FUNC_DEF(gpu, slice9, 3),
MIST_FUNC_DEF(gpu, make_sprite_mesh, 2), MIST_FUNC_DEF(gpu, make_sprite_mesh, 2),
MIST_FUNC_DEF(gpu, make_sprite_queue, 4), MIST_FUNC_DEF(gpu, make_sprite_queue, 4),
MIST_FUNC_DEF(geometry, renderitem_push, 3),
MIST_FUNC_DEF(geometry, renderitem_sort, 0),
MIST_FUNC_DEF(geometry, renderitem_clear, 0),
}; };
JSValue js_geometry_use(JSContext *js) { JSValue js_geometry_use(JSContext *js) {