diff --git a/scripts/core/_sdl_video.js b/scripts/core/_sdl_video.js index 947fd356..d2ed2431 100644 --- a/scripts/core/_sdl_video.js +++ b/scripts/core/_sdl_video.js @@ -348,6 +348,22 @@ function handle_renderer(msg) { ); 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); @@ -404,13 +420,35 @@ function handle_renderer(msg) { return {id: surf_id}; case 'loadTexture': - if (!msg.data || !msg.data.surface_id) return {error: "Missing surface_id"}; - var surf = resources.surface[msg.data.surface_id]; - if (!surf) return {error: "Invalid surface id"}; - var tex = ren.load_texture(surf); + if (!msg.data) return {error: "Missing data"}; + + var tex; + // Load from surface ID + if (msg.data.surface_id) { + var surf = resources.surface[msg.data.surface_id]; + if (!surf) return {error: "Invalid surface id"}; + tex = ren.load_texture(surf); + } + // Load from raw surface object (for graphics module) + else if (msg.data.surface) { + tex = ren.load_texture(msg.data); + } + // Direct surface data + else if (msg.data.width && msg.data.height) { + tex = ren.load_texture(msg.data); + } + else { + return {error: "Must provide surface_id or surface data"}; + } + + if (!tex) return {error: "Failed to load texture"}; var tex_id = allocate_id(); resources.texture[tex_id] = tex; - return {id: tex_id}; + return { + id: tex_id, + width: tex.width, + height: tex.height + }; case 'createTexture': if (!msg.data || !msg.data.width || !msg.data.height) { diff --git a/scripts/core/engine.js b/scripts/core/engine.js index dc10f2e1..99707eb7 100644 --- a/scripts/core/engine.js +++ b/scripts/core/engine.js @@ -1,6 +1,12 @@ (function engine() { prosperon.DOC = Symbol('+documentation+') // Symbol for documentation references +globalThis.log = new Proxy({}, { + get(target,prop,receiver) { + return function() {} + } +}) + var listeners = new Map() prosperon.on = function(type, callback) { @@ -672,7 +678,7 @@ function handle_message(msg) { delete msg.data letter[HEADER] = msg if (msg.return) { - console.log(`Received a message for the return id ${msg.return}`) + log.trace(`Received a message for the return id ${msg.return}`) var fn = replies[msg.return] if (!fn) throw new Error(`Could not find return function for message ${msg.return}`) fn(letter) diff --git a/scripts/modules/draw2d.js b/scripts/modules/draw2d.js index 5ed93060..6c857f9f 100644 --- a/scripts/modules/draw2d.js +++ b/scripts/modules/draw2d.js @@ -1,8 +1,12 @@ -var graphics = use('graphics') +var renderer_actor = arg[0] +var renderer_id = arg[1] + +var graphics = use('graphics', renderer_actor, renderer_id) var math = use('math') var util = use('util') var os = use('os') var geometry = use('geometry') +var Color = use('color') var draw = {} draw[prosperon.DOC] = ` @@ -13,23 +17,10 @@ for lines, rectangles, text, sprite drawing, etc. Immediate mode. // Draw command accumulator var commands = [] -// Renderer info set by moth -var renderer_actor = null -var renderer_id = null - // Prototype object for commands -var command_proto = null - -// Set the renderer -draw.set_renderer = function(actor, id) { - renderer_actor = actor - renderer_id = id - - // Create prototype object with common fields - command_proto = { - kind: "renderer", - id: id - } +var command_proto = { + kind: "renderer", + id: renderer_id } // Clear accumulated commands @@ -484,12 +475,58 @@ draw.image = function image(image, rect = [0,0], rotation = 0, anchor = [0,0], s if (!image) throw Error('Need an image to render.') if (typeof image === "string") image = graphics.texture(image) - rect.width ??= image.texture.width - rect.height ??= image.texture.height - info ??= image_info; - // TODO: Handle texture loading and sending texture_id - // For now, we skip image rendering as it requires texture management + // Ensure rect has proper structure + if (Array.isArray(rect)) { + rect = {x: rect[0], y: rect[1], width: image.width, height: image.height} + } else { + rect.width ??= image.width + rect.height ??= image.height + } + + info = Object.assign({}, image_info, info); + + // Get the GPU texture (might be loading) + var texture = image.gpu; + if (!texture) { + // Texture not loaded yet, skip drawing + return; + } + + // Set texture filtering mode + if (info.mode) { + add_command("set", null, "textureFilter", info.mode === 'linear' ? 'linear' : 'nearest') + } + + // Set color if specified + if (info.color) { + add_command("set", null, "drawColor", info.color) + } + + // Calculate source rectangle from image.rect (UV coords) + var src_rect = { + x: image.rect.x * texture.width, + y: image.rect.y * texture.height, + width: image.rect.width * texture.width, + height: image.rect.height * texture.height + } + + // Handle flipping + if (info.flip_x) { + src_rect.x += src_rect.width; + src_rect.width = -src_rect.width; + } + if (info.flip_y) { + src_rect.y += src_rect.height; + src_rect.height = -src_rect.height; + } + + // Draw the texture + add_command("copyTexture", { + texture_id: texture.id, + src: src_rect, + dest: rect + }) } function software_circle(pos, radius) diff --git a/scripts/modules/graphics.js b/scripts/modules/graphics.js index 9e224691..f52eae08 100644 --- a/scripts/modules/graphics.js +++ b/scripts/modules/graphics.js @@ -6,14 +6,18 @@ Includes both JavaScript and C-implemented routines for creating geometry buffer rectangle packing, etc. ` +var renderer_actor = arg[0] +var renderer_id = arg[1] + var io = use('io') var os = use('os') var res = use('resources') -var render = use('render') +var json = use('json') var GPU = Symbol() var CPU = Symbol() var LASTUSE = Symbol() +var LOADING = Symbol() var cache = new Map() @@ -21,9 +25,26 @@ var cache = new Map() graphics.Image = { get gpu() { this[LASTUSE] = os.now(); - if (!this[GPU]) { - this[GPU] = render.load_texture(this[CPU]); - decorate_rect_px(this); + if (!this[GPU] && !this[LOADING]) { + this[LOADING] = true; + var self = this; + + // Send message to load texture + send(renderer_actor, { + kind: "renderer", + id: renderer_id, + op: "loadTexture", + data: this[CPU] + }, function(response) { + if (response.error) { + console.error("Failed to load texture:", response.error); + self[LOADING] = false; + } else { + self[GPU] = response; + decorate_rect_px(self); + self[LOADING] = false; + } + }); } return this[GPU] @@ -33,18 +54,23 @@ graphics.Image = { get cpu() { this[LASTUSE] = os.now(); - if (!this[CPU]) this[CPU] = render.read_texture(this[GPU]) + // Note: Reading texture back from GPU requires async operation + // For now, return the CPU data if available return this[CPU] }, get surface() { return this.cpu }, get width() { - return this[GPU].width + if (this[GPU]) return this[GPU].width + if (this[CPU]) return this[CPU].width + return 0 }, get height() { - return this[GPU].height + if (this[GPU]) return this[GPU].height + if (this[CPU]) return this[CPU].height + return 0 }, unload_gpu() { @@ -79,15 +105,11 @@ function decorate_rect_px(img) { function make_handle(obj) { - var image = Object.create(graphics.Image); - - if (obj.surface) { - im - } return Object.assign(Object.create(graphics.Image), { rect:{x:0,y:0,width:1,height:1}, [CPU]:obj, [GPU]:undefined, + [LOADING]:false, [LASTUSE]:os.now() }) } @@ -188,19 +210,18 @@ graphics.texture_from_data = function(data) { if (!(data instanceof ArrayBuffer)) return undefined - var img = { - surface: graphics.make_texture(data) - } - render.load_texture(img) - decorate_rect_px(img) + var surface = graphics.make_texture(data); + var img = make_handle(surface); - return img + // Trigger GPU load (async) + img.gpu; + + return img; } graphics.from_surface = function(id, surf) { return make_handle(surf) - var img = { surface: surf } } graphics.from = function(id, data) @@ -307,10 +328,22 @@ graphics.get_font = function get_font(path, size) { var data = io.slurpbytes(fullpath) var font = graphics.make_font(data,size) - font.texture = render.load_texture(font.surface) - console.log('loaded font texture') - console.log(json.encode(font.texture)) + // Load font texture via renderer actor (async) + send(renderer_actor, { + kind: "renderer", + id: renderer_id, + op: "loadTexture", + data: font.surface + }, function(response) { + if (response.error) { + console.error("Failed to load font texture:", response.error); + } else { + font.texture = response; + console.log('loaded font texture'); + console.log(json.encode(font.texture)); + } + }); fontcache[fontstr] = font @@ -341,23 +374,6 @@ Builds a single geometry mesh for all sprite-type commands in the queue, storing so they can be rendered in one draw call. ` -graphics.make_sprite_mesh[prosperon.DOC] = ` -:param sprites: An array of sprite objects, each containing .rect (or transform), .src (UV region), .color, etc. -:param oldMesh (optional): An existing mesh object to reuse/resize if possible. -:return: A GPU mesh object with pos, uv, color, and indices buffers for all sprites. -Given an array of sprites, build a single geometry mesh for rendering them. -` - -graphics.make_sprite_queue[prosperon.DOC] = ` -:param sprites: An array of sprite objects. -:param camera: (unused in the C code example) Typically a camera or transform for sorting? -:param pipeline: A pipeline object for rendering. -:param sort: An integer or boolean for whether to sort sprites; if truthy, sorts by layer & texture. -:return: An array of pipeline commands: geometry with mesh references, grouped by image. -Given an array of sprites, optionally sort them, then build a queue of pipeline commands. -Each group with a shared image becomes one command. -` - graphics.make_text_buffer[prosperon.DOC] = ` :param text: The string to render. :param rect: A rectangle specifying position and possibly wrapping. diff --git a/scripts/modules/render.js b/scripts/modules/render.js deleted file mode 100644 index a2069c9f..00000000 --- a/scripts/modules/render.js +++ /dev/null @@ -1 +0,0 @@ -return use('sdl_render') diff --git a/scripts/modules/sdl_render.js b/scripts/modules/sdl_render.js deleted file mode 100644 index 30dd36e0..00000000 --- a/scripts/modules/sdl_render.js +++ /dev/null @@ -1,125 +0,0 @@ -var render = {} - -var context - -var util = use('util') - -render.initialize = function(config) -{ - 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" - } - - config.__proto__ = default_conf - prosperon.window = prosperon.engine_start(config) - context = prosperon.window.make_renderer() - context.logical_size([config.resolution_x, config.resolution_y], config.mode) -} - -render.sprite = function(sprite) -{ - context.sprite(sprite) -} - -// img here is the engine surface -render.load_texture = function(surface) -{ - return context.load_texture(surface) -} - -var current_color = Color.white - -render.image = function(image, rect, rotation, anchor, shear, info) -{ -// rect.width = image.rect_px.width; -// rect.height = image.rect_px.height; - image.texture.mode(info.mode) - context.texture(image.texture, image.rect_px, rect, rotation, anchor); -} - -render.clip = function(rect) -{ - context.clip(rect) -} - -render.line = function(points) -{ - context.line(points) -} - -render.point = function(pos) -{ - context.point(pos) -} - -render.rectangle = function(rect) -{ - context.rects([rect]) -} - -render.rects = function(rects) -{ - context.rects(rects) -} - -render.pipeline = function(pipe) -{ - // any changes here -} - -render.settings = function(set) -{ - if (!set.color) return - context.draw_color(set.color) -} - -render.geometry = function(image, mesh, pipeline) -{ - context.geometry(image, mesh) -} - -render.slice9 = function(image, rect, slice, info, pipeline) -{ - context.slice9(image.texture, image.rect_px, util.normalizeSpacing(slice), rect); -} - -render.get_image = function(rect) -{ - return context.get_image(rect) -} - -render.clear = function(color) -{ - if (color) context.draw_color(color) - context.clear() -} - -render.present = function() -{ - context.present() -} - -render.camera = function(cam) -{ - context.camera(cam); -} - -return render diff --git a/source/prosperon.c b/source/prosperon.c index 74981d54..340f02d5 100644 --- a/source/prosperon.c +++ b/source/prosperon.c @@ -592,6 +592,7 @@ void script_startup(prosperon_rt *prt, void (*hook)(JSContext*)) JS_AddIntrinsicMapSet(js); JS_AddIntrinsicTypedArrays(js); JS_AddIntrinsicWeakRef(js); + JS_AddIntrinsicProxy(js); JS_SetContextOpaque(js, prt); prt->context = js; diff --git a/tests/draw2d.js b/tests/draw2d.js new file mode 100644 index 00000000..e7b7d48d --- /dev/null +++ b/tests/draw2d.js @@ -0,0 +1,238 @@ +// Test draw2d module without moth framework +var draw2d +var graphics +var os = use('os'); + +use('tracy').level = 1 + +// Create SDL video actor +var video = use('sdl_video'); +var video_actor = {__ACTORDATA__:{id:video}}; + +var window_id = null; +var renderer_id = null; + +// Create window +send(video_actor, { + kind: "window", + op: "create", + data: { + title: "Draw2D Test", + width: 800, + height: 600 + } +}, function(response) { + if (response.error) { + console.error("Failed to create window:", response.error); + return; + } + + window_id = response.id; + console.log("Created window with id:", window_id); + + // Create renderer + send(video_actor, { + kind: "window", + op: "makeRenderer", + id: window_id + }, function(response) { + if (response.error) { + console.error("Failed to create renderer:", response.error); + return; + } + + renderer_id = response.id; + console.log("Created renderer with id:", renderer_id); + + // Configure draw2d and graphics + draw2d = use('draw2d', video_actor, renderer_id) + graphics = use('graphics', video_actor, renderer_id) + + // Start drawing after a short delay + $_.delay(start_drawing, 0.1); + }); +}); + +function start_drawing() { + var frame = 0; + var start_time = os.now(); + + // Load an image + var bunny_image = null; + try { + bunny_image = graphics.texture('tests/bunny.png'); + console.log("Loaded bunny image"); + } catch (e) { + console.error("Failed to load bunny image:", e); + } + + function draw_frame() { + frame++; + var t = os.now() - start_time; + + // Clear the screen with a dark background + send(video_actor, { + kind: "renderer", + id: renderer_id, + op: "set", + prop: "drawColor", + value: [0.1, 0.1, 0.15, 1] + }); + send(video_actor, { + kind: "renderer", + id: renderer_id, + op: "clear" + }); + + // Clear draw2d commands + draw2d.clear(); + + // Draw some rectangles + draw2d.rectangle( + {x: 50, y: 50, width: 100, height: 100}, + {thickness: 0, color: [1, 0, 0, 1]} + ); + + draw2d.rectangle( + {x: 200, y: 50, width: 100, height: 100}, + {thickness: 5, color: [0, 1, 0, 1]} + ); + + draw2d.rectangle( + {x: 350, y: 50, width: 100, height: 100}, + {thickness: 2, color: [0, 0, 1, 1], radius: 20} + ); + + // Draw circles with animation + var radius = 30 + Math.sin(t * 2) * 10; + draw2d.circle( + [100, 250], + radius, + {color: [1, 1, 0, 1], thickness: 0} + ); + + draw2d.circle( + [250, 250], + 40, + {color: [1, 0, 1, 1], thickness: 3} + ); + + // Draw ellipse + draw2d.ellipse( + [400, 250], + [60, 30], + {color: [0, 1, 1, 1], thickness: 2} + ); + + // Draw lines + var line_y = 350 + Math.sin(t * 3) * 20; + draw2d.line( + [[50, line_y], [150, line_y + 50], [250, line_y]], + {color: [1, 0.5, 0, 1], thickness: 2} + ); + + // Draw cross + draw2d.cross( + [350, 375], + 25, + {color: [0.5, 1, 0.5, 1], thickness: 3} + ); + + // Draw arrow + draw2d.arrow( + [450, 350], + [550, 400], + 15, + 30, + {color: [1, 1, 1, 1], thickness: 2} + ); + + // Draw partial circle (arc) + draw2d.circle( + [150, 480], + 50, + { + color: [0.8, 0.8, 1, 1], + thickness: 5, + start: 0.25, + end: 0.75 + } + ); + + // Draw filled partial ellipse + draw2d.ellipse( + [350, 480], + [80, 40], + { + color: [1, 0.8, 0.8, 1], + thickness: 0, + start: 0, + end: 0.6 + } + ); + + // Draw some points in a pattern + var point_count = 20; + for (var i = 0; i < point_count; i++) { + var angle = (i / point_count) * Math.PI * 2; + var r = 30 + Math.sin(t * 4 + i * 0.5) * 10; + var px = 650 + Math.cos(angle) * r; + var py = 300 + Math.sin(angle) * r; + + draw2d.point( + [px, py], + 3, + {color: [1, 0.5 + Math.sin(t * 2 + i) * 0.5, 0.5, 1]} + ); + } + + // Draw the bunny image if loaded + if (bunny_image) { + // Static bunny + draw2d.image(bunny_image, {x: 500, y: 450, width: 64, height: 64}); + + // Rotating bunny + var rotation = t * 0.5; + draw2d.image( + bunny_image, + {x: 600, y: 450, width: 64, height: 64}, + rotation, + [0.5, 0.5] // Center anchor + ); + + // Bouncing bunny with tint + var bounce_y = 500 + Math.sin(t * 3) * 20; + draw2d.image( + bunny_image, + {x: 700, y: bounce_y, width: 48, height: 48}, + 0, + [0.5, 1], // Bottom center anchor + [0, 0], // No shear + {color: [1, 0.5, 0.5, 1]} // Red tint + ); + } + + // Flush all commands to renderer + draw2d.flush(); + + // Present the frame + send(video_actor, { + kind: "renderer", + id: renderer_id, + op: "present" + }); + + // Schedule next frame (60 FPS) + if (frame < 600) { // Run for 10 seconds + $_.delay(draw_frame, 1/60); + } else { + console.log("Test completed - drew", frame, "frames"); + $_.delay($_.stop, 0.5); + } + } + + draw_frame(); +} + +// Stop after 12 seconds if not already stopped +$_.delay($_.stop, 12); \ No newline at end of file