diff --git a/Cargo.lock b/Cargo.lock index 0ea1625..cb1eecc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -642,7 +642,6 @@ dependencies = [ "haku", "log", "paste", - "tiny-skia", ] [[package]] @@ -886,12 +885,6 @@ version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" -[[package]] -name = "libm" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" - [[package]] name = "libsqlite3-sys" version = "0.30.1" @@ -1652,7 +1645,6 @@ checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" dependencies = [ "arrayref", "bytemuck", - "libm", "strict-num", ] diff --git a/Cargo.toml b/Cargo.toml index 5fb2332..8e04f74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ haku.path = "crates/haku" haku2.path = "crates/haku2" log = "0.4.22" rkgk-image-ops.path = "crates/rkgk-image-ops" -tiny-skia = { version = "0.11.4", default-features = false } [profile.dev.package.rkgk-image-ops] opt-level = 3 diff --git a/crates/haku-wasm/Cargo.toml b/crates/haku-wasm/Cargo.toml index cb4c19c..9b87472 100644 --- a/crates/haku-wasm/Cargo.toml +++ b/crates/haku-wasm/Cargo.toml @@ -11,7 +11,6 @@ arrayvec = { version = "0.7.4", default-features = false } dlmalloc = { version = "0.2.6", features = ["global"] } haku.workspace = true log.workspace = true -tiny-skia = { workspace = true, features = ["no-std-float"] } paste = "1.0.15" [features] diff --git a/crates/haku-wasm/src/lib.rs b/crates/haku-wasm/src/lib.rs index 5030c58..c882252 100644 --- a/crates/haku-wasm/src/lib.rs +++ b/crates/haku-wasm/src/lib.rs @@ -16,10 +16,6 @@ use haku::{ token::Lexis, }; use log::{debug, info}; -use tiny_skia::{ - BlendMode, Color, FillRule, LineCap, Paint, PathBuilder, Pixmap, PremultipliedColorU8, Rect, - Shader, Stroke, Transform, -}; pub mod logging; #[cfg(not(feature = "std"))] @@ -201,147 +197,6 @@ extern "C" fn haku_status_string(code: StatusCode) -> *const i8 { .as_ptr() } -struct PixmapCanvas { - pixmap: Pixmap, - pb: PathBuilder, - transform: Transform, -} - -#[unsafe(no_mangle)] -extern "C" fn haku_pixmap_new(width: u32, height: u32) -> *mut PixmapCanvas { - let ptr = Box::leak(Box::new(PixmapCanvas { - pixmap: Pixmap::new(width, height).expect("invalid pixmap size"), - pb: PathBuilder::new(), - transform: Transform::identity(), - })) as *mut _; - debug!("created pixmap with size {width}x{height}: {ptr:?}"); - ptr -} - -#[unsafe(no_mangle)] -unsafe extern "C" fn haku_pixmap_destroy(c: *mut PixmapCanvas) { - debug!("destroying pixmap: {c:?}"); - drop(Box::from_raw(c)) -} - -#[unsafe(no_mangle)] -unsafe extern "C" fn haku_pixmap_data(c: *mut PixmapCanvas) -> *mut u8 { - let c = &mut *c; - c.pixmap.pixels_mut().as_mut_ptr() as *mut u8 -} - -#[unsafe(no_mangle)] -unsafe extern "C" fn haku_pixmap_clear(c: *mut PixmapCanvas) { - let c = &mut *c; - c.pixmap - .pixels_mut() - .fill(PremultipliedColorU8::TRANSPARENT); -} - -#[unsafe(no_mangle)] -unsafe extern "C" fn haku_pixmap_set_translation(c: *mut PixmapCanvas, x: f32, y: f32) { - let c = &mut *c; - c.transform = Transform::from_translate(x, y); -} - -#[unsafe(no_mangle)] -unsafe extern "C" fn haku_pixmap_begin(c: *mut PixmapCanvas) -> bool { - let c = &mut *c; - c.pb.clear(); - true -} - -#[unsafe(no_mangle)] -unsafe extern "C" fn haku_pixmap_line( - c: *mut PixmapCanvas, - x1: f32, - y1: f32, - x2: f32, - y2: f32, -) -> bool { - let c = &mut *c; - c.pb.move_to(x1, y1); - c.pb.line_to(x2, y2); - true -} - -#[unsafe(no_mangle)] -unsafe extern "C" fn haku_pixmap_rectangle( - c: *mut PixmapCanvas, - x: f32, - y: f32, - width: f32, - height: f32, -) -> bool { - let c = &mut *c; - if let Some(rect) = Rect::from_xywh(x, y, width, height) { - c.pb.push_rect(rect); - } - true -} - -#[unsafe(no_mangle)] -unsafe extern "C" fn haku_pixmap_circle(c: *mut PixmapCanvas, x: f32, y: f32, r: f32) -> bool { - let c = &mut *c; - c.pb.push_circle(x, y, r); - true -} - -fn default_paint() -> Paint<'static> { - Paint { - shader: Shader::SolidColor(Color::BLACK), - blend_mode: BlendMode::SourceOver, - anti_alias: false, - force_hq_pipeline: false, - } -} - -#[unsafe(no_mangle)] -unsafe extern "C" fn haku_pixmap_fill(c: *mut PixmapCanvas, r: u8, g: u8, b: u8, a: u8) -> bool { - let c = &mut *c; - let pb = mem::take(&mut c.pb); - if let Some(path) = pb.finish() { - let paint = Paint { - shader: Shader::SolidColor(Color::from_rgba8(r, g, b, a)), - ..default_paint() - }; - c.pixmap - .fill_path(&path, &paint, FillRule::EvenOdd, c.transform, None); - } - true -} - -#[unsafe(no_mangle)] -unsafe extern "C" fn haku_pixmap_stroke( - c: *mut PixmapCanvas, - r: u8, - g: u8, - b: u8, - a: u8, - thickness: f32, -) -> bool { - let c = &mut *c; - let pb = mem::take(&mut c.pb); - if let Some(path) = pb.finish() { - let paint = Paint { - shader: Shader::SolidColor(Color::from_rgba8(r, g, b, a)), - ..default_paint() - }; - c.pixmap.stroke_path( - &path, - &paint, - &Stroke { - width: thickness, - line_cap: LineCap::Round, - ..Default::default() - }, - c.transform, - None, - ); - } - true -} - #[unsafe(no_mangle)] unsafe extern "C" fn haku_compile_brush( instance: *mut Instance, diff --git a/static/brush-preview.js b/static/brush-preview.js index 97a2015..cca3150 100644 --- a/static/brush-preview.js +++ b/static/brush-preview.js @@ -1,5 +1,3 @@ -import { Pixmap } from "rkgk/haku.js"; - export class BrushPreview extends HTMLElement { constructor(width, height) { super(); @@ -9,53 +7,11 @@ export class BrushPreview extends HTMLElement { } connectedCallback() { - this.canvas = this.appendChild(document.createElement("canvas")); - this.ctx = this.canvas.getContext("2d"); - - this.#resizeCanvas(); - if (this.width == null || this.height == null) { - new ResizeObserver(() => this.#resizeCanvas()).observe(this); - } - } - - #resizeCanvas() { - this.canvas.width = this.width ?? this.clientWidth; - this.canvas.height = this.height ?? this.clientHeight; - - // This can happen if the element's `display: none`. - if (this.canvas.width == 0 || this.canvas.height == 0) return; - - if (this.pixmap != null) { - this.pixmap.destroy(); - } - this.pixmap = new Pixmap(this.canvas.width, this.canvas.height); - - this.dispatchEvent(new Event(".pixmapLost")); + // TODO } async #renderBrushInner(haku) { - this.pixmap.clear(); - let evalResult = await haku.evalBrush({ - runDotter: async () => { - return { - fromX: this.canvas.width / 2, - fromY: this.canvas.height / 2, - toX: this.canvas.width / 2, - toY: this.canvas.height / 2, - num: 0, - }; - }, - - runScribble: async (renderToPixmap) => { - return renderToPixmap(this.pixmap, 0, 0); - }, - }); - if (evalResult.status != "ok") { - return { status: "error", phase: "eval", result: evalResult }; - } - - this.ctx.putImageData(this.pixmap.getImageData(), 0, 0); - + // TODO return { status: "ok" }; } diff --git a/static/canvas-renderer.js b/static/canvas-renderer.js index 8c934df..5fa38fa 100644 --- a/static/canvas-renderer.js +++ b/static/canvas-renderer.js @@ -1,6 +1,7 @@ import { listen, Pool } from "rkgk/framework.js"; import { Viewport } from "rkgk/viewport.js"; import { Wall, chunkKey } from "rkgk/wall.js"; +import { AtlasAllocator } from "rkgk/chunk-allocator.js"; class CanvasRenderer extends HTMLElement { viewport = new Viewport(); @@ -135,7 +136,8 @@ class CanvasRenderer extends HTMLElement { out vec4 f_color; void main() { - f_color = texture(u_texture, vf_uv); + vec4 color = texture(u_texture, vf_uv); + f_color = color; } `, ); @@ -191,8 +193,7 @@ class CanvasRenderer extends HTMLElement { uboRects: this.uboRects, }); - this.atlasAllocator = new AtlasAllocator(this.wall.chunkSize, 8); - this.chunkAllocations = new Map(); + this.atlasAllocator = new AtlasAllocator(this.gl, this.wall.chunkSize, 8); console.debug("initialized atlas allocator", this.atlasAllocator); @@ -298,15 +299,11 @@ class CanvasRenderer extends HTMLElement { for (let batch of this.batches) { for (let [i, chunks] of batch) { let atlas = this.atlasAllocator.atlases[i]; - this.gl.bindTexture(this.gl.TEXTURE_2D, atlas.id); + this.gl.bindTexture(this.gl.TEXTURE_2D, atlas.texture); this.#resetRectBuffer(); for (let chunk of chunks) { - let { i, allocation } = this.getChunkAllocation( - chunk.layerId, - chunk.x, - chunk.y, - ); + let { i, allocation } = chunk.allocation; let atlas = this.atlasAllocator.atlases[i]; this.#pushRect( chunk.x * this.wall.chunkSize, @@ -326,42 +323,20 @@ class CanvasRenderer extends HTMLElement { // TODO: This is a nice debug view. // There should be a switch to it somewhere in the app. /* - let x = 0; - let y = 0; - for (let atlas of this.atlasAllocator.atlases) { - this.#resetRectBuffer(); - this.gl.bindTexture(this.gl.TEXTURE_2D, atlas.id); - this.#pushRect(x, y, atlas.textureSize, atlas.textureSize, 0, 0, 1, 1); - this.#drawRects(); - if (x > atlas.textureSize * 16) { - y += atlas.textureSize; - x = 0; - } - x += atlas.textureSize; - } - */ - } - - getChunkAllocation(layerId, chunkX, chunkY) { - let key = `${layerId}/${chunkKey(chunkX, chunkY)}`; - if (this.chunkAllocations.has(key)) { - return this.chunkAllocations.get(key); - } else { - let allocation = this.atlasAllocator.alloc(this.gl); - this.chunkAllocations.set(key, allocation); - return allocation; - } - } - - deallocateChunks(layer) { - for (let chunkKey of layer.chunks.keys()) { - let key = `${layer.id}/${chunkKey}`; - if (this.chunkAllocations.has(key)) { - let allocation = this.chunkAllocations.get(key); - this.atlasAllocator.dealloc(allocation); - this.chunkAllocations.delete(key); + let x = 0; + let y = 0; + for (let atlas of this.atlasAllocator.atlases) { + this.#resetRectBuffer(); + this.gl.bindTexture(this.gl.TEXTURE_2D, atlas.texture); + this.#pushRect(x, y, atlas.textureSize, atlas.textureSize, 0, 0, 1, 1); + this.#drawRects(); + if (x > atlas.textureSize * 16) { + y += atlas.textureSize; + x = 0; } + x += atlas.textureSize; } + // */ } #collectChunksThisFrame() { @@ -383,12 +358,7 @@ class CanvasRenderer extends HTMLElement { for (let chunkX = left; chunkX < right; ++chunkX) { let chunk = layer.getChunk(chunkX, chunkY); if (chunk != null) { - if (chunk.renderDirty) { - this.#updateChunkTexture(layer, chunkX, chunkY); - chunk.renderDirty = false; - } - - let allocation = this.getChunkAllocation(layer.id, chunkX, chunkY); + let allocation = chunk.id; let array = batch.get(allocation.i); if (array == null) { @@ -396,7 +366,7 @@ class CanvasRenderer extends HTMLElement { batch.set(allocation.i, array); } - array.push({ layerId: layer.id, x: chunkX, y: chunkY }); + array.push({ layerId: layer.id, x: chunkX, y: chunkY, allocation }); } } } @@ -438,12 +408,6 @@ class CanvasRenderer extends HTMLElement { this.gl.drawArraysInstanced(this.gl.TRIANGLES, 0, 6, this.uboRectsNum); } - #updateChunkTexture(layer, chunkX, chunkY) { - let allocation = this.getChunkAllocation(layer.id, chunkX, chunkY); - let chunk = layer.getChunk(chunkX, chunkY); - this.atlasAllocator.upload(this.gl, allocation, chunk.pixmap); - } - // Behaviours sendViewportUpdate() { @@ -584,101 +548,3 @@ class InteractEvent extends Event { } } } - -class Atlas { - static getInitBuffer(chunkSize, nChunks) { - let imageSize = chunkSize * nChunks; - return new Uint8Array(imageSize * imageSize * 4); - } - - constructor(gl, chunkSize, nChunks, initBuffer) { - this.id = gl.createTexture(); - this.chunkSize = chunkSize; - this.nChunks = nChunks; - this.textureSize = chunkSize * nChunks; - - this.free = Array(nChunks * nChunks); - for (let y = 0; y < nChunks; ++y) { - for (let x = 0; x < nChunks; ++x) { - this.free[x + y * nChunks] = { x, y }; - } - } - - gl.bindTexture(gl.TEXTURE_2D, this.id); - gl.texImage2D( - gl.TEXTURE_2D, - 0, - gl.RGBA8, - this.textureSize, - this.textureSize, - 0, - gl.RGBA, - gl.UNSIGNED_BYTE, - initBuffer, - ); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); - gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); - } - - alloc() { - return this.free.pop(); - } - - dealloc(xy) { - this.free.push(xy); - } - - upload(gl, { x, y }, pixmap) { - gl.bindTexture(gl.TEXTURE_2D, this.id); - gl.texSubImage2D( - gl.TEXTURE_2D, - 0, - x * this.chunkSize, - y * this.chunkSize, - this.chunkSize, - this.chunkSize, - gl.RGBA, - gl.UNSIGNED_BYTE, - pixmap.getArrayBuffer(), - ); - } -} - -class AtlasAllocator { - atlases = []; - - constructor(chunkSize, nChunks) { - this.chunkSize = chunkSize; - this.nChunks = nChunks; - this.initBuffer = Atlas.getInitBuffer(chunkSize, nChunks); - } - - alloc(gl) { - // Right now we do a dumb linear scan through all atlases, but in the future it would be - // really nice to optimize this by storing information about which atlases have free slots - // precisely. - - for (let i = 0; i < this.atlases.length; ++i) { - let atlas = this.atlases[i]; - let allocation = atlas.alloc(); - if (allocation != null) { - return { i, allocation }; - } - } - - let i = this.atlases.length; - let atlas = new Atlas(gl, this.chunkSize, this.nChunks, this.initBuffer); - let allocation = atlas.alloc(); - this.atlases.push(atlas); - return { i, allocation }; - } - - dealloc({ i, allocation }) { - let atlas = this.atlases[i]; - atlas.dealloc(allocation); - } - - upload(gl, { i, allocation }, pixmap) { - this.atlases[i].upload(gl, allocation, pixmap); - } -} diff --git a/static/chunk-allocator.js b/static/chunk-allocator.js new file mode 100644 index 0000000..849ed65 --- /dev/null +++ b/static/chunk-allocator.js @@ -0,0 +1,194 @@ +class Atlas { + static getInitBuffer(chunkSize, nChunks) { + let imageSize = chunkSize * nChunks; + return new Uint8Array(imageSize * imageSize * 4).fill(0xaa); + } + + constructor(gl, chunkSize, nChunks, initBuffer) { + this.chunkSize = chunkSize; + this.nChunks = nChunks; + this.textureSize = chunkSize * nChunks; + + this.free = Array(nChunks * nChunks); + for (let y = 0; y < nChunks; ++y) { + for (let x = 0; x < nChunks; ++x) { + this.free[x + y * nChunks] = { x, y }; + } + } + + this.texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, this.texture); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA8, + this.textureSize, + this.textureSize, + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + initBuffer, + ); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + + this.framebuffer = gl.createFramebuffer(); + gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); + gl.framebufferTexture2D( + gl.FRAMEBUFFER, + gl.COLOR_ATTACHMENT0, + gl.TEXTURE_2D, + this.texture, + 0, + ); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + } + + alloc() { + return this.free.pop(); + } + + dealloc(xy) { + this.free.push(xy); + } + + upload(gl, { x, y }, source) { + gl.bindTexture(gl.TEXTURE_2D, this.texture); + gl.texSubImage2D( + gl.TEXTURE_2D, + 0, + x * this.chunkSize, + y * this.chunkSize, + this.chunkSize, + this.chunkSize, + gl.RGBA, + gl.UNSIGNED_BYTE, + source, + ); + } + + // Assumes a pack PBO is bound. + download(gl, { x, y }) { + gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer); + gl.readPixels( + x * this.chunkSize, + y * this.chunkSize, + this.chunkSize, + this.chunkSize, + gl.RGBA, + gl.UNSIGNED_BYTE, + null, + ); + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + } +} + +export class AtlasAllocator { + atlases = []; + + // Download buffers + #pboPool = []; + #downloadBufferPool = []; + #pendingDownloads = []; + + constructor(gl, chunkSize, nChunks) { + this.gl = gl; + this.chunkSize = chunkSize; + this.nChunks = nChunks; + this.initBuffer = Atlas.getInitBuffer(chunkSize, nChunks); + } + + alloc() { + // Right now we do a dumb linear scan through all atlases, but in the future it would be + // really nice to optimize this by storing information about which atlases have free slots + // precisely. + + for (let i = 0; i < this.atlases.length; ++i) { + let atlas = this.atlases[i]; + let allocation = atlas.alloc(); + if (allocation != null) { + return { i, allocation }; + } + } + + let i = this.atlases.length; + let atlas = new Atlas(this.gl, this.chunkSize, this.nChunks, this.initBuffer); + let allocation = atlas.alloc(); + this.atlases.push(atlas); + + return { i, allocation }; + } + + dealloc(id) { + let { i, allocation } = id; + let atlas = this.atlases[i]; + atlas.dealloc(allocation); + } + + upload(id, source) { + let { i, allocation } = id; + this.atlases[i].upload(this.gl, allocation, source); + } + + async download(id) { + let gl = this.gl; + + // Get PBO + + let pbo = this.#pboPool.pop(); + if (pbo == null) { + let dataSize = this.chunkSize * this.chunkSize * 4; + pbo = gl.createBuffer(); + gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo); + gl.bufferData(gl.PIXEL_PACK_BUFFER, dataSize, gl.DYNAMIC_READ); + } + + // Initiate download + + gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo); + this.atlases[id.i].download(gl, id); + let fence = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0); + + gl.bindBuffer(gl.PIXEL_PACK_BUFFER, 0); + + // Add for ticking + + return new Promise((resolve) => { + this.#pendingDownloads.push({ pbo, fence, resolve }); + }); + } + + // Call after download() finishes running to give memory back to the allocator, for reuse in + // later pixel transfers. + freeDownloaded(arrayBuffer) { + this.#downloadBufferPool.push(arrayBuffer); + } + + // Call every frame to poll for download completion. + tickDownloads() { + let gl = this.gl; + + for (let i = 0; i < this.#pendingDownloads.length; ++i) { + let pending = this.#pendingDownloads[i]; + let status = gl.getSyncParameter(pending.fence, gl.SYNC_STATUS); + if (status == gl.SIGNALED) { + // Transfer complete, fetch pixels back to an array buffer. + let dataSize = this.chunkSize * this.chunkSize * 4; + let arrayBuffer = this.#downloadBufferPool.pop() ?? new ArrayBuffer(dataSize); + + gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pending.pbo); + gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, arrayBuffer); + gl.bindBuffer(gl.PIXEL_PACK_BUFFER, 0); + gl.deleteSync(pending.fence); + + pending.resolve(arrayBuffer); + + let last = this.#pendingDownloads.pop(); + if (last != null) { + this.#pendingDownloads[i] = last; + --i; + } + } + } + } +} diff --git a/static/haku.js b/static/haku.js index a9532d0..4f6625c 100644 --- a/static/haku.js +++ b/static/haku.js @@ -160,40 +160,6 @@ canvasStrokeImpl = w.haku_pixmap_stroke; w.haku_init_logging(); -export class Pixmap { - #pPixmap = 0; - - constructor(width, height) { - this.#pPixmap = allocCheck(w.haku_pixmap_new(width, height)); - this.width = width; - this.height = height; - } - - destroy() { - w.haku_pixmap_destroy(this.#pPixmap); - } - - clear(r, g, b, a) { - w.haku_pixmap_clear(this.#pPixmap, r, g, b, a); - } - - get ptr() { - return this.#pPixmap; - } - - getArrayBuffer() { - return new Uint8ClampedArray( - memory.buffer, - w.haku_pixmap_data(this.#pPixmap), - this.width * this.height * 4, - ); - } - - getImageData() { - return new ImageData(this.getArrayBuffer(), this.width, this.height); - } -} - export const ContKind = { Scribble: 0, Dotter: 1, diff --git a/static/index.js b/static/index.js index d04c4dc..f968cf8 100644 --- a/static/index.js +++ b/static/index.js @@ -146,6 +146,7 @@ function readUrl(urlString) { } let currentUser = wall.onlineUsers.getUser(session.sessionId); + let chunkAllocator = canvasRenderer.atlasAllocator; // Event loop @@ -214,15 +215,13 @@ function readUrl(urlString) { updatePromises.push( createImageBitmap(blob).then((bitmap) => { let chunk = wall.mainLayer.getOrCreateChunk( + chunkAllocator, info.position.x, info.position.y, ); if (chunk == null) return; - chunk.ctx.globalCompositeOperation = "copy"; - chunk.ctx.drawImage(bitmap, 0, 0); - chunk.syncToPixmap(); - chunk.markModified(); + chunk.upload(chunkAllocator, bitmap); }), ); } diff --git a/static/wall.js b/static/wall.js index 4c8f846..2e26764 100644 --- a/static/wall.js +++ b/static/wall.js @@ -1,29 +1,20 @@ -import { Pixmap } from "rkgk/haku.js"; import { OnlineUsers } from "rkgk/online-users.js"; export class Chunk { - constructor(size) { - this.pixmap = new Pixmap(size, size); - this.canvas = new OffscreenCanvas(size, size); - this.ctx = this.canvas.getContext("2d"); - this.renderDirty = false; + constructor(chunkAllocator) { + this.id = chunkAllocator.alloc(); } - destroy() { - this.pixmap.destroy(); + destroy(chunkAllocator) { + chunkAllocator.dealloc(this.id); } - syncFromPixmap() { - this.ctx.putImageData(this.pixmap.getImageData(), 0, 0); + upload(chunkAllocator, source) { + chunkAllocator.upload(this.id, source); } - syncToPixmap() { - let imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); - this.pixmap.getImageData().data.set(imageData.data, 0); - } - - markModified() { - this.renderDirty = true; + download(chunkAllocator) { + return chunkAllocator.download(this.id); } } @@ -51,50 +42,52 @@ export class Layer { return this.chunks.get(chunkKey(x, y))?.chunk; } - getOrCreateChunk(x, y) { + getOrCreateChunk(chunkAllocator, x, y) { let key = chunkKey(x, y); if (this.chunks.has(key)) { return this.chunks.get(key)?.chunk; } else { if (this.chunkLimit != null && this.chunks.size >= this.chunkLimit) return null; - let chunk = new Chunk(this.chunkSize); + let chunk = new Chunk(chunkAllocator); this.chunks.set(key, { x, y, chunk }); return chunk; } } compositeAlpha(src) { - for (let { x, y, chunk: srcChunk } of src.chunks.values()) { - srcChunk.syncFromPixmap(); - let dstChunk = this.getOrCreateChunk(x, y); - if (dstChunk == null) continue; - - dstChunk.ctx.globalCompositeOperation = "source-over"; - dstChunk.ctx.drawImage(srcChunk.canvas, 0, 0); - dstChunk.syncToPixmap(); - dstChunk.markModified(); - } + // TODO + // for (let { x, y, chunk: srcChunk } of src.chunks.values()) { + // srcChunk.syncFromPixmap(); + // let dstChunk = this.getOrCreateChunk(x, y); + // if (dstChunk == null) continue; + // dstChunk.ctx.globalCompositeOperation = "source-over"; + // dstChunk.ctx.drawImage(srcChunk.canvas, 0, 0); + // dstChunk.syncToPixmap(); + // dstChunk.markModified(); + // } } async toEdits() { let edits = []; - let start = performance.now(); + // TODO - for (let { x, y, chunk } of this.chunks.values()) { - edits.push({ - chunk: { x, y }, - data: chunk.canvas.convertToBlob({ type: "image/png" }), - }); - } + // let start = performance.now(); - for (let edit of edits) { - edit.data = await edit.data; - } + // for (let { x, y, chunk } of this.chunks.values()) { + // edits.push({ + // chunk: { x, y }, + // data: chunk.canvas.convertToBlob({ type: "image/png" }), + // }); + // } - let end = performance.now(); - console.debug("toEdits done", end - start); + // for (let edit of edits) { + // edit.data = await edit.data; + // } + + // let end = performance.now(); + // console.debug("toEdits done", end - start); return edits; }