diff --git a/prosperon/prosperon.cm b/prosperon/prosperon.cm index 648db8fc..383d43fb 100644 --- a/prosperon/prosperon.cm +++ b/prosperon/prosperon.cm @@ -201,7 +201,7 @@ function translate_draw_commands(commands) { renderer_commands.push({ op: "texture", data: { - texture_id: gpu.id, + texture_id: gpu, dst: cmd.rect, src: img.rect } @@ -233,7 +233,7 @@ function translate_draw_commands(commands) { renderer_commands.push({ op: "texture9Grid", data: { - texture_id: gpu.id, + texture_id: gpu, src: img.rect, leftWidth: cmd.slice, rightWidth: cmd.slice, @@ -274,7 +274,7 @@ function translate_draw_commands(commands) { num_vertices: geom.num_vertices, num_indices: geom.num_indices, size_indices: geom.size_indices, - texture_id: gpu.id + texture_id: gpu } renderer_commands.push({ @@ -285,9 +285,17 @@ function translate_draw_commands(commands) { break case "geometry": - var img = graphics.texture(cmd.image) - var gpu = img.gpu - if (!gpu) break + var texture_id + if (cmd.texture_id) { + // 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 var geom = cmd.geometry @@ -308,7 +316,7 @@ function translate_draw_commands(commands) { num_vertices: geom.num_vertices, num_indices: geom.num_indices, size_indices: geom.size_indices, - texture_id: gpu.id + texture_id: texture_id } renderer_commands.push({ diff --git a/prosperon/tilemap.cm b/prosperon/tilemap.cm index 34253ad2..dc4067db 100644 --- a/prosperon/tilemap.cm +++ b/prosperon/tilemap.cm @@ -7,7 +7,7 @@ function tilemap() this.offset_y = 0; this.size_x = 32; this.size_y = 32; - this._geometry_cache = null; + this._geometry_cache = {}; // Cache actual geometry data by texture this._dirty = true; return this; } @@ -65,6 +65,12 @@ tilemap.prototype = // Ensure array exists up to x 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 this.tiles[x][y] = image; @@ -76,8 +82,10 @@ tilemap.prototype = _build_geometry_cache(pos = {x: 0, y: 0}) { var geometry = use('geometry'); - // Group tiles by texture + // Group tiles by texture (using a unique key per image object) var textureGroups = {}; + var imageToKey = new Map(); // Map image objects to unique keys + var keyCounter = 0; // Collect all tiles and their positions 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++) { var tile = this.tiles[x][y]; 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]) { textureGroups[textureKey] = { tiles: [], + image: tile, // Store the image object offset_x: this.offset_x, offset_y: this.offset_y, size_x: this.size_x, @@ -104,8 +121,8 @@ tilemap.prototype = } } - // Generate geometry for each texture group - var geometryCommands = []; + // Generate and cache geometry for each texture group + this._geometry_cache = {}; for (var textureKey in textureGroups) { var group = textureGroups[textureKey]; if (group.tiles.length == 0) continue; @@ -129,29 +146,38 @@ tilemap.prototype = tempMap.tiles[arrayX][arrayY] = image; }); - // Generate geometry for this group + // Generate and cache geometry for this group var geom = geometry.tilemap_to_data(tempMap); - - geometryCommands.push({ - cmd: "geometry", + this._geometry_cache[textureKey] = { geometry: geom, - image: textureKey - }); + image: group.image + }; } - this._geometry_cache = geometryCommands; this._dirty = false; }, draw(pos = {x: 0, y: 0}) { // 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._last_pos = {x: pos.x, y: pos.y}; } - // Return cached geometry commands - return this._geometry_cache; + // Generate commands from cached geometry, pulling texture_id dynamically + 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; }, } diff --git a/scripts/graphics.cm b/scripts/graphics.cm index 342912c4..accba96a 100644 --- a/scripts/graphics.cm +++ b/scripts/graphics.cm @@ -19,94 +19,97 @@ var LASTUSE = Symbol() var LOADING = Symbol() var cache = {} +var pending_gpu_loads = [] graphics.setup = function(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 graphics.Image = function(surfaceData) { - // Initialize private properties - this[CPU] = surfaceData || null; - this[GPU] = null; + // Initialize properties + this.cpu = surfaceData || 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[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 -Object.defineProperties(graphics.Image.prototype, { - gpu: { - get: function() { - if (!this[GPU] && !this[LOADING] && renderer_actor) { - this[LOADING] = true; - var self = this; - - // Send message to load texture - send(renderer_actor, { - kind: "renderer", - op: "loadTexture", - data: this[CPU] - }, function(response) { - if (response.error) { - log.error("Failed to load texture:") - log.error(response.error) - self[LOADING] = false; - } else { - self[GPU] = response; - decorate_rect_px(self); - self[LOADING] = false; - } - }); +graphics.Image.prototype.loadGPU = function() { + if (!this[LOADING] && renderer_actor && this.cpu) { + this[LOADING] = true; + var self = this; + + send(renderer_actor, { + kind: "renderer", + op: "loadTexture", + data: this.cpu + }, function(response) { + if (response.error) { + log.error("Failed to load texture:") + log.error(response.error) + self[LOADING] = false; + } else { + // Store the full response as texture (has width/height) + self.texture = response; + // Store just the ID as gpu + self.gpu = response.id || response; + decorate_rect_px(self); + 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 graphics.Image.prototype.unload_gpu = function() { - this[GPU] = null + this.gpu = 0; + this.texture = 0; } graphics.Image.prototype.unload_cpu = function() { - this[CPU] = null + this.cpu = null; + this.surface = null; } function calc_image_size(img) { if (!img.rect) return if (img.texture) { return [img.texture.width * img.rect.width, img.texture.height * img.rect.height] - } else if (img[CPU]) { - return [img[CPU].width * img.rect.width, img[CPU].height * img.rect.height] + } else if (img.cpu) { + return [img.cpu.width * img.rect.width, img.cpu.height * img.rect.height] } return [0, 0] } @@ -119,9 +122,9 @@ function decorate_rect_px(img) { if (img.texture) { width = img.texture.width; height = img.texture.height; - } else if (img[CPU]) { - width = img[CPU].width; - height = img[CPU].height; + } else if (img.cpu) { + width = img.cpu.width; + height = img.cpu.height; } else { return; } @@ -148,11 +151,15 @@ function wrapSurface(surf, maybeRect){ return h; } function wrapFrames(arr){ /* [{surface,time,rect}, …] → [{image,time}] */ - return arr.map(f => ({ - image : wrapSurface(f.surface || f), /* accept bare surface too */ - time: f.time, - rect: f.rect /* keep for reference */ - })); + return arr.map(f => { + // Handle both surface objects and objects with surface property + var surf = f.surface || f; + return { + image: wrapSurface(surf), + time: f.time || 0, + rect: f.rect /* keep for reference */ + } + }); } function makeAnim(frames, loop=true){ return { frames, loop } @@ -161,13 +168,10 @@ function makeAnim(frames, loop=true){ function decode_image(bytes, ext) { switch(ext) { - case 'gif': - var g = graphics.make_gif(bytes) - if (g.frames) return g.frames[0] - return g + case 'gif': return graphics.make_gif(bytes) // returns array of surfaces case 'ase': 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{ def bytes = io.slurpbytes(path); - let raw = decode_image(bytes, path.ext()); + let raw = decode_image(bytes, path.ext()); - /* ── Case A: static image ─────────────────────────────────── */ - if(raw.surface) { - var gg = new graphics.Image(raw.surface) - return gg + /* ── Case A: single surface (from make_texture) ────────────── */ + if(raw && raw.width && raw.pixels && !Array.isArray(raw)) { + return new graphics.Image(raw) } - /* ── Case B: GIF helpers returned array [surf, …] ─────────── */ - if(Array.isArray(raw)) - return makeAnim( wrapFrames(raw), true ); - - /* ── Case C: GIF helpers returned {frames,loop} ───────────── */ - if(raw.frames && Array.isArray(raw.frames)) - return makeAnim( wrapFrames(raw.frames), !!raw.loop ); - - /* ── Case D: ASE helpers returned { animName:{frames,loop}, … } ── */ - 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) /* ase with flat surface */ - anims[name] = makeAnim( - [{image:make_handle(anim.surface),time:0}], true ); + /* ── Case B: array of surfaces (from make_gif) ────────────── */ + if(Array.isArray(raw)) { + // Single frame GIF returns array with one surface + if(raw.length == 1 && !raw[0].time) { + return new graphics.Image(raw[0]) + } + // Multiple frames - create animation + return makeAnim(wrapFrames(raw), true); + } + + /* ── Case C: ASE helpers returned { animName:{frames,loop}, … } or single frame ── */ + if(typeof raw == 'object' && !raw.width) { + // Check if it's a single surface from ASE (single frame, no tags) + if(raw.surface) { + return new graphics.Image(raw.surface) + } + + // 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'); @@ -216,9 +235,9 @@ image.dimensions = function() { if (this.texture) { width = this.texture.width; height = this.texture.height; - } else if (this[CPU]) { - width = this[CPU].width; - height = this[CPU].height; + } else if (this.cpu) { + width = this.cpu.width; + height = this.cpu.height; } return [width, height].scale([this.rect[2], this.rect[3]]) } @@ -287,9 +306,9 @@ graphics.texture = function texture(path) { if (!ipath) throw new Error(`unknown image ${id}`) - var image = create_image(ipath) - cache[id] = image - return image + var result = create_image(ipath) + cache[id] = result + return result // Can be Image, animation, or collection of animations } graphics.texture[cell.DOC] = ` :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] // Preserve the GPU texture ID if it exists - var oldGPU = oldimg[GPU] + var oldGPU = oldimg.gpu // Update the CPU surface data - oldimg[CPU] = img[CPU] + oldimg.cpu = img.cpu + oldimg.surface = img.cpu // Clear GPU texture to force reload - oldimg[GPU] = null + oldimg.gpu = 0 + oldimg.texture = 0 oldimg[LOADING] = false // Update dimensions - if (img[CPU]) { - oldimg.rect = {x:0, y:0, width:img[CPU].width, height:img[CPU].height} + if (img.cpu) { + 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) } // If the texture was on GPU, trigger reload if (oldGPU && renderer_actor) { - oldimg.gpu // This getter will trigger the reload + oldimg.loadGPU() } } graphics.tex_hotreload[cell.DOC] = ` diff --git a/source/jsffi.c b/source/jsffi.c index 7f0840b5..97c70257 100644 --- a/source/jsffi.c +++ b/source/jsffi.c @@ -1078,28 +1078,16 @@ JSC_CCALL(os_make_gif, int height; void *pixels = stbi_load_gif_from_memory(raw, rawlen, &delays, &width, &height, &frames, &n, 4); - JSValue gif = JS_NewObject(js); - ret = gif; - - 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; + if (!pixels) { + return JS_ThrowReferenceError(js, "Failed to decode GIF: %s", stbi_failure_reason()); } - 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++) { - JSValue frame = JS_NewObject(js); - JS_SetPropertyStr(js, frame, "time", number2js(js,(float)delays[i]/1000.0)); - - // Create surface data object instead of SDL_Surface + // Create 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)); @@ -1109,15 +1097,17 @@ JSC_CCALL(os_make_gif, 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, frame, "surface", surfData); - JS_SetPropertyUint32(js, delay_arr, i, frame); + // Add time property for animation frames + 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: - free(delays); - free(pixels); + if (delays) free(delays); + if (pixels) free(pixels); ) JSValue aseframe2js(JSContext *js, ase_frame_t aframe) @@ -1153,6 +1143,19 @@ JSC_CCALL(os_make_aseprite, JSValue obj = aseframe2js(js,ase->frames[0]); cute_aseprite_free(ase); 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; } } diff --git a/source/qjs_geometry.c b/source/qjs_geometry.c index dd376f67..5aa26300 100644 --- a/source/qjs_geometry.c +++ b/source/qjs_geometry.c @@ -1242,6 +1242,82 @@ JSC_CCALL(geometry_transform_xy_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[] = { MIST_FUNC_DEF(geometry, rect_intersection, 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, make_sprite_mesh, 2), 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) {