From bb55e23979f9558aed7a11cf48c92697590dbc67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=AA=E3=82=AD=E8=90=8C?= Date: Fri, 5 Sep 2025 20:20:45 +0200 Subject: [PATCH] initial implementation of WebGL-based brush renderer --- crates/haku/src/system.rs | 6 -- crates/haku2/src/canvas.zig | 48 +++-------- crates/haku2/src/render.zig | 23 ++--- crates/haku2/src/system.zig | 60 ++----------- crates/haku2/src/value.zig | 48 ++--------- static/brush-renderer.js | 168 ++++++++++++++++++++++++++++++++++++ static/canvas-renderer.js | 61 ++++--------- static/chunk-allocator.js | 87 ++++++++++++++++--- static/haku.js | 37 +++----- static/index.js | 8 +- static/online-users.js | 3 +- static/painter.js | 34 ++++++-- static/wall.js | 4 +- static/webgl.js | 45 ++++++++++ 14 files changed, 385 insertions(+), 247 deletions(-) create mode 100644 static/brush-renderer.js create mode 100644 static/webgl.js diff --git a/crates/haku/src/system.rs b/crates/haku/src/system.rs index d75a450..aef6348 100644 --- a/crates/haku/src/system.rs +++ b/crates/haku/src/system.rs @@ -77,13 +77,7 @@ pub fn resolve(arity: SystemFnArity, name: &str) -> Option { (Nary, "reduce") => 0x95, (Nary, "flatten") => 0x96, - (Nary, "toShape") => 0xc0, - (Nary, "line") => 0xc1, - (Nary, "rect") => 0xc2, - (Nary, "circle") => 0xc3, - (Nary, "stroke") => 0xe0, - (Nary, "fill") => 0xe1, (Nary, "withDotter") => 0xf0, diff --git a/crates/haku2/src/canvas.zig b/crates/haku2/src/canvas.zig index cfd32cb..42d4ec8 100644 --- a/crates/haku2/src/canvas.zig +++ b/crates/haku2/src/canvas.zig @@ -8,41 +8,21 @@ pub const Canvas = opaque { if (!status) return error.Draw; } - pub fn begin(c: *Canvas) !void { - try wrap(__haku2_canvas_begin(c)); - } - - pub fn line(c: *Canvas, start: value.Vec2, end: value.Vec2) !void { - const x1, const y1 = start; - const x2, const y2 = end; - try wrap(__haku2_canvas_line(c, x1, y1, x2, y2)); - } - - pub fn rect(c: *Canvas, top_left: value.Vec2, size: value.Vec2) !void { - const x, const y = top_left; - const width, const height = size; - try wrap(__haku2_canvas_rectangle(c, x, y, width, height)); - } - - pub fn circle(c: *Canvas, center: value.Vec2, r: f32) !void { - const x, const y = center; - try wrap(__haku2_canvas_circle(c, x, y, r)); - } - - pub fn fill(c: *Canvas, color: value.Rgba8) !void { + pub fn stroke(c: *Canvas, color: value.Rgba8, thickness: f32, from: value.Vec2, to: value.Vec2) !void { const r, const g, const b, const a = color; - try wrap(__haku2_canvas_fill(c, r, g, b, a)); - } - - pub fn stroke(c: *Canvas, color: value.Rgba8, thickness: f32) !void { - const r, const g, const b, const a = color; - try wrap(__haku2_canvas_stroke(c, r, g, b, a, thickness)); + try wrap(__haku2_canvas_stroke(c, r, g, b, a, thickness, from[0], from[1], to[0], to[1])); } }; -extern fn __haku2_canvas_begin(c: *Canvas) bool; -extern fn __haku2_canvas_line(c: *Canvas, x1: f32, y1: f32, x2: f32, y2: f32) bool; -extern fn __haku2_canvas_rectangle(c: *Canvas, x: f32, y: f32, width: f32, height: f32) bool; -extern fn __haku2_canvas_circle(c: *Canvas, x: f32, y: f32, r: f32) bool; -extern fn __haku2_canvas_fill(c: *Canvas, r: u8, g: u8, b: u8, a: u8) bool; -extern fn __haku2_canvas_stroke(c: *Canvas, r: u8, g: u8, b: u8, a: u8, thickness: f32) bool; +extern fn __haku2_canvas_stroke( + c: *Canvas, + r: u8, + g: u8, + b: u8, + a: u8, + thickness: f32, + from_x: f32, + from_y: f32, + to_x: f32, + to_y: f32, +) bool; diff --git a/crates/haku2/src/render.zig b/crates/haku2/src/render.zig index 08e2de4..d9087f5 100644 --- a/crates/haku2/src/render.zig +++ b/crates/haku2/src/render.zig @@ -24,18 +24,13 @@ fn renderRec(vm: *Vm, canvas: *Canvas, val: Value, depth: usize, max_depth: usiz switch (val.ref.*) { .scribble => { - try canvas.begin(); - - switch (val.ref.scribble.shape) { - .point => |point| try canvas.line(point, point), - .line => |line| try canvas.line(line.start, line.end), - .rect => |rect| try canvas.rect(rect.top_left, rect.size), - .circle => |circle| try canvas.circle(circle.center, circle.radius), - } - - switch (val.ref.scribble.action) { - .stroke => |stroke| try canvas.stroke(value.rgbaTo8(stroke.color), stroke.thickness), - .fill => |fill| try canvas.fill(value.rgbaTo8(fill.color)), + switch (val.ref.scribble) { + .stroke => |stroke| try canvas.stroke( + value.rgbaTo8(stroke.color), + stroke.thickness, + value.vec2From4(stroke.from), + value.vec2From4(stroke.to), + ), } }, @@ -46,10 +41,6 @@ fn renderRec(vm: *Vm, canvas: *Canvas, val: Value, depth: usize, max_depth: usiz } }, - .shape => { - return vm.throw("the brush returned a bare shape, which cannot be drawn. try wrapping your shape in a fill or a stroke: (fill #000 )", .{}); - }, - else => return notAScribble(vm, val), } } diff --git a/crates/haku2/src/system.zig b/crates/haku2/src/system.zig index e165031..e5edbc5 100644 --- a/crates/haku2/src/system.zig +++ b/crates/haku2/src/system.zig @@ -118,14 +118,6 @@ fn fromArgument(cx: Context, comptime T: type, i: usize) Vm.Error!T { if (val != .ref or val.ref.* != .list) return typeError(cx.vm, val, i, "list"); return val.ref.list; }, - value.Shape => { - const val = cx.args[i]; - if (toShape(val)) |shape| { - return shape; - } else { - return typeError(cx.vm, val, i, "shape"); - } - }, *const value.Closure => { const val = cx.args[i]; if (val != .ref or val.ref.* != .closure) return typeError(cx.vm, val, i, "function"); @@ -282,12 +274,7 @@ pub const fns = makeFnTable(&[_]SparseFn{ .{ 0x94, erase("filter", filter) }, .{ 0x95, erase("reduce", reduce) }, .{ 0x96, erase("flatten", flatten) }, - .{ 0xc0, erase("toShape", valueToShape) }, - .{ 0xc1, erase("line", line) }, - .{ 0xc2, erase("rect", rect) }, - .{ 0xc3, erase("circle", circle) }, .{ 0xe0, erase("stroke", stroke) }, - .{ 0xe1, erase("fill", fill) }, .{ 0xf0, erase("withDotter", withDotter) }, }); @@ -738,51 +725,14 @@ fn flatten(list: value.List, cx: Context) Vm.Error!value.Ref { return .{ .list = flattened_list }; } -fn toShape(val: value.Value) ?value.Shape { - return switch (val) { - .nil, .false, .true, .tag, .number, .rgba => null, - .vec4 => |v| .{ .point = value.vec2From4(v) }, - .ref => |r| if (r.* == .shape) r.shape else null, - }; -} - -/// `toShape` -fn valueToShape(val: value.Value) ?value.Ref { - if (toShape(val)) |shape| { - return .{ .shape = shape }; - } else { - return null; - } -} - -fn line(start: Vec4, end: Vec4) value.Ref { - return .{ .shape = .{ .line = .{ - .start = value.vec2From4(start.value), - .end = value.vec2From4(end.value), - } } }; -} - -fn rect(top_left: Vec4, size: Vec4) value.Ref { - return .{ .shape = .{ .rect = .{ - .top_left = value.vec2From4(top_left.value), - .size = value.vec2From4(size.value), - } } }; -} - -fn circle(center: Vec4, radius: f32) value.Ref { - return .{ .shape = .{ .circle = .{ - .center = value.vec2From4(center.value), - .radius = radius, - } } }; -} - -fn stroke(thickness: f32, color: Rgba, shape: value.Shape) value.Ref { +fn stroke(thickness: f32, color: Rgba, from: Vec4, to: Vec4) value.Ref { return .{ .scribble = .{ - .shape = shape, - .action = .{ .stroke = .{ + .stroke = .{ .thickness = thickness, .color = color.value, - } }, + .from = from.value, + .to = to.value, + }, } }; } diff --git a/crates/haku2/src/value.zig b/crates/haku2/src/value.zig index 3269a60..fdb8c47 100644 --- a/crates/haku2/src/value.zig +++ b/crates/haku2/src/value.zig @@ -62,7 +62,6 @@ pub const Value = union(enum) { .ref => |r| switch (r.*) { .closure => "function", .list => "list", - .shape => "shape", .scribble => "scribble", .reticle => "reticle", }, @@ -86,7 +85,7 @@ pub const Value = union(enum) { } try writer.writeAll("]"); }, - inline .shape, .scribble, .reticle => |x| try writer.print("{}", .{x}), + inline .scribble, .reticle => |x| try writer.print("{}", .{x}), }, } } @@ -122,7 +121,6 @@ pub fn rgbaTo8(rgba: Rgba) Rgba8 { pub const Ref = union(enum) { closure: Closure, list: List, - shape: Shape, scribble: Scribble, reticle: Reticle, }; @@ -161,44 +159,14 @@ pub const Closure = struct { pub const List = []Value; -pub const Shape = union(enum) { - point: Vec2, - line: Line, - rect: Rect, - circle: Circle, +pub const Scribble = union(enum) { + stroke: Stroke, - pub const Line = struct { - start: Vec2, - end: Vec2, - }; - - pub const Rect = struct { - top_left: Vec2, - size: Vec2, - }; - - pub const Circle = struct { - center: Vec2, - radius: f32, - }; -}; - -pub const Scribble = struct { - shape: Shape, - action: Action, - - pub const Action = union(enum) { - stroke: Stroke, - fill: Fill, - - pub const Stroke = struct { - thickness: f32, - color: Rgba, - }; - - pub const Fill = struct { - color: Rgba, - }; + pub const Stroke = struct { + thickness: f32, + color: Rgba, + from: Vec4, + to: Vec4, }; }; diff --git a/static/brush-renderer.js b/static/brush-renderer.js new file mode 100644 index 0000000..e1500a3 --- /dev/null +++ b/static/brush-renderer.js @@ -0,0 +1,168 @@ +import { compileProgram, orthographicProjection } from "rkgk/webgl.js"; + +const linesVertexShader = `#version 300 es + precision highp float; + + uniform mat4 u_projection; + uniform vec2 u_translation; + + layout (location = 0) in vec2 a_position; + // Instance + layout (location = 1) in vec4 a_line; // (x1, y1, x2, y2) + layout (location = 2) in vec4 a_color; + layout (location = 3) in vec2 a_properties; // (thickness, hardness) + + void main() { + float thickness = a_properties.x; + float hardness = a_properties.y; + + vec2 xAxis = a_line.zw - a_line.xy; + vec2 direction = normalize(xAxis); + vec2 yAxis = vec2(-direction.y, direction.x) * thickness; + + vec2 localPosition = a_line.xy + xAxis * a_position.x + yAxis * a_position.y; + vec4 screenPosition = vec4(localPosition + u_translation, 0.0, 1.0); + vec4 scenePosition = screenPosition * u_projection; + + gl_Position = scenePosition; + } +`; + +const linesFragmentShader = `#version 300 es + precision highp float; + + out vec4 f_color; + + void main() { + f_color = vec4(vec3(0.0), 1.0); + } +`; + +const linesMaxInstances = 1; +const lineInstanceSize = 12; +const lineDataBufferSize = lineInstanceSize * linesMaxInstances; + +export class BrushRenderer { + #translation = { x: 0, y: 0 }; + + constructor(gl, canvasSource) { + this.gl = gl; + this.canvasSource = canvasSource; + + console.group("construct BrushRenderer"); + + // Lines + + let linesProgramId = compileProgram(gl, linesVertexShader, linesFragmentShader); + this.linesProgram = { + id: linesProgramId, + u_projection: gl.getUniformLocation(linesProgramId, "u_projection"), + u_translation: gl.getUniformLocation(linesProgramId, "u_translation"), + }; + + this.linesVao = gl.createVertexArray(); + this.linesVbo = gl.createBuffer(); + + gl.bindVertexArray(this.linesVao); + gl.bindBuffer(gl.ARRAY_BUFFER, this.linesVbo); + + const lineRect = new Float32Array( + // prettier-ignore + [ + 0, -0.5, + 1, -0.5, + 0, 0.5, + 1, -0.5, + 1, 0.5, + 0, 0.5, + ], + ); + + this.linesVboData = new Float32Array(lineRect.length + lineDataBufferSize); + this.linesVboData.set(lineRect, 0); + this.linesInstanceData = this.linesVboData.subarray(lineRect.length); + + gl.bufferData(gl.ARRAY_BUFFER, this.linesVboData, gl.DYNAMIC_DRAW, 0); + + gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 2 * 4, 0); // a_position + gl.vertexAttribPointer(1, 4, gl.FLOAT, false, lineInstanceSize, lineRect.byteLength); // a_line + gl.vertexAttribPointer( + 2, // a_color + 4, + gl.FLOAT, + false, + lineInstanceSize, + lineRect.byteLength + 4 * 4, + ); + gl.vertexAttribPointer( + 3, // a_properties + 2, + gl.FLOAT, + false, + lineInstanceSize, + lineRect.byteLength + 4 * 4 * 2, + ); + for (let i = 0; i < 4; ++i) gl.enableVertexAttribArray(i); + for (let i = 1; i < 4; ++i) gl.vertexAttribDivisor(i, 1); + + console.debug("pipeline lines", { + linesVao: this.linesVao, + linesVbo: this.linesVbo, + linesVboSize: this.linesVboData.byteLength, + linesInstanceDataOffset: this.linesInstanceData.byteOffset, + }); + + gl.bindVertexArray(null); + + console.groupEnd(); + } + + #drawLines(instanceCount) { + let gl = this.gl; + + gl.bindVertexArray(this.linesVao); + gl.bindBuffer(gl.ARRAY_BUFFER, this.linesVbo); + gl.bufferSubData( + gl.ARRAY_BUFFER, + this.linesInstanceData.byteOffset, + this.linesInstanceData.subarray(0, instanceCount * lineInstanceSize), + ); + gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, instanceCount); + } + + setTranslation(x, y) { + this.#translation.x = x; + this.#translation.y = y; + } + + stroke(canvas, r, g, b, a, thickness, x1, y1, x2, y2) { + let gl = this.gl; + + let viewport = this.canvasSource.useCanvas(gl, canvas); + + gl.useProgram(this.linesProgram.id); + gl.uniformMatrix4fv( + this.linesProgram.u_projection, + false, + orthographicProjection(0, viewport.width, viewport.height, 0, -1, 1), + ); + gl.uniform2f(this.linesProgram.u_translation, this.#translation.x, this.#translation.y); + + let instances = this.linesInstanceData; + instances[0] = x1; + instances[1] = y1; + instances[2] = x2; + instances[3] = y2; + instances[4] = r / 255; + instances[5] = g / 255; + instances[6] = b / 255; + instances[7] = a / 255; + instances[8] = thickness; + instances[9] = 1; // hardness + this.#drawLines(1); + + this.canvasSource.resetCanvas(gl); + + return true; + } +} diff --git a/static/canvas-renderer.js b/static/canvas-renderer.js index 5fa38fa..26fcb48 100644 --- a/static/canvas-renderer.js +++ b/static/canvas-renderer.js @@ -2,6 +2,8 @@ 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"; +import { compileProgram } from "rkgk/webgl.js"; +import { BrushRenderer } from "rkgk/brush-renderer.js"; class CanvasRenderer extends HTMLElement { viewport = new Viewport(); @@ -74,7 +76,7 @@ class CanvasRenderer extends HTMLElement { // Renderer initialization #initializeRenderer() { - console.groupCollapsed("initializeRenderer"); + console.group("initializeRenderer"); console.info("vendor", this.gl.getParameter(this.gl.VENDOR)); console.info("renderer", this.gl.getParameter(this.gl.RENDERER)); @@ -93,7 +95,9 @@ class CanvasRenderer extends HTMLElement { // We also realistically don't need anymore, because (at least at the time I'm writing this) // we store (8 * 8 = 64) chunks per texture atlas, so we can't batch more than that. const maxRects = 64; - let renderChunksProgramId = this.#compileProgram( + let renderChunksProgramId = compileProgram( + this.gl, + // Vertex `#version 300 es @@ -200,47 +204,13 @@ class CanvasRenderer extends HTMLElement { this.batches = []; this.batchPool = new Pool(); + this.brushRenderer = new BrushRenderer(this.gl, this.atlasAllocator.canvasSource()); + console.debug("GL error state", this.gl.getError()); console.groupEnd(); } - #compileShader(kind, source) { - let shader = this.gl.createShader(kind); - - this.gl.shaderSource(shader, source); - this.gl.compileShader(shader); - - if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) { - let error = new Error(`failed to compile shader: ${this.gl.getShaderInfoLog(shader)}`); - this.gl.deleteShader(shader); - throw error; - } else { - return shader; - } - } - - #compileProgram(vertexSource, fragmentSource) { - let vertexShader = this.#compileShader(this.gl.VERTEX_SHADER, vertexSource); - let fragmentShader = this.#compileShader(this.gl.FRAGMENT_SHADER, fragmentSource); - - let program = this.gl.createProgram(); - this.gl.attachShader(program, vertexShader); - this.gl.attachShader(program, fragmentShader); - this.gl.linkProgram(program); - - this.gl.deleteShader(vertexShader); - this.gl.deleteShader(fragmentShader); - - if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) { - let error = new Error(`failed to link program: ${this.gl.getProgramInfoLog(program)}`); - this.gl.deleteProgram(program); - throw error; - } else { - return program; - } - } - // Renderer #render() { @@ -256,6 +226,7 @@ class CanvasRenderer extends HTMLElement { } this.gl.viewport(0, 0, this.canvas.width, this.canvas.height); + this.gl.scissor(0, 0, this.canvas.width, this.canvas.height); this.gl.clearColor(1, 1, 1, 1); this.gl.clear(this.gl.COLOR_BUFFER_BIT); @@ -303,8 +274,9 @@ class CanvasRenderer extends HTMLElement { this.#resetRectBuffer(); for (let chunk of chunks) { - let { i, allocation } = chunk.allocation; - let atlas = this.atlasAllocator.atlases[i]; + let atlasIndex = this.atlasAllocator.getAtlasIndex(chunk.id); + let allocation = this.atlasAllocator.getAllocation(chunk.id); + let atlas = this.atlasAllocator.atlases[atlasIndex]; this.#pushRect( chunk.x * this.wall.chunkSize, chunk.y * this.wall.chunkSize, @@ -358,15 +330,14 @@ class CanvasRenderer extends HTMLElement { for (let chunkX = left; chunkX < right; ++chunkX) { let chunk = layer.getChunk(chunkX, chunkY); if (chunk != null) { - let allocation = chunk.id; - - let array = batch.get(allocation.i); + let atlasIndex = this.atlasAllocator.getAtlasIndex(chunk.id); + let array = batch.get(atlasIndex); if (array == null) { array = []; - batch.set(allocation.i, array); + batch.set(atlasIndex, array); } - array.push({ layerId: layer.id, x: chunkX, y: chunkY, allocation }); + array.push({ layerId: layer.id, x: chunkX, y: chunkY, id: chunk.id }); } } } diff --git a/static/chunk-allocator.js b/static/chunk-allocator.js index 849ed65..48a64b7 100644 --- a/static/chunk-allocator.js +++ b/static/chunk-allocator.js @@ -1,7 +1,7 @@ class Atlas { static getInitBuffer(chunkSize, nChunks) { let imageSize = chunkSize * nChunks; - return new Uint8Array(imageSize * imageSize * 4).fill(0xaa); + return new Uint8Array(imageSize * imageSize * 4).fill(0x00); } constructor(gl, chunkSize, nChunks, initBuffer) { @@ -44,8 +44,12 @@ class Atlas { gl.bindFramebuffer(gl.FRAMEBUFFER, null); } - alloc() { - return this.free.pop(); + alloc(gl, initBuffer) { + let xy = this.free.pop(); + if (xy != null) { + this.upload(gl, xy, initBuffer); + } + return xy; } dealloc(xy) { @@ -81,11 +85,24 @@ class Atlas { ); gl.bindFramebuffer(gl.FRAMEBUFFER, null); } + + getFramebufferRect({ x, y }) { + return { + x: x * this.chunkSize, + y: y * this.chunkSize, + width: this.chunkSize, + height: this.chunkSize, + }; + } } export class AtlasAllocator { atlases = []; + // Allocation names + #ids = new Map(); + #idCounter = 1; + // Download buffers #pboPool = []; #downloadBufferPool = []; @@ -98,6 +115,28 @@ export class AtlasAllocator { this.initBuffer = Atlas.getInitBuffer(chunkSize, nChunks); } + #obtainId(allocInfo) { + let id = this.#idCounter++; + this.#ids.set(id, allocInfo); + return id; + } + + #releaseId(id) { + this.#ids.delete(id); + } + + #getAllocInfo(id) { + return this.#ids.get(id); + } + + getAtlasIndex(id) { + return this.#getAllocInfo(id).i; + } + + getAllocation(id) { + return this.#getAllocInfo(id).allocation; + } + 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 @@ -105,33 +144,35 @@ export class AtlasAllocator { for (let i = 0; i < this.atlases.length; ++i) { let atlas = this.atlases[i]; - let allocation = atlas.alloc(); + let allocation = atlas.alloc(this.gl, this.initBuffer); if (allocation != null) { - return { i, allocation }; + return this.#obtainId({ i, allocation }); } } let i = this.atlases.length; let atlas = new Atlas(this.gl, this.chunkSize, this.nChunks, this.initBuffer); - let allocation = atlas.alloc(); + let allocation = atlas.alloc(this.gl, this.initBuffer); this.atlases.push(atlas); - return { i, allocation }; + return this.#obtainId({ i, allocation }); } dealloc(id) { - let { i, allocation } = id; + let { i, allocation } = this.#getAllocInfo(id); let atlas = this.atlases[i]; atlas.dealloc(allocation); + this.#releaseId(id); } upload(id, source) { - let { i, allocation } = id; + let { i, allocation } = this.#getAllocInfo(id); this.atlases[i].upload(this.gl, allocation, source); } async download(id) { let gl = this.gl; + let allocInfo = this.#getAllocInfo(id); // Get PBO @@ -146,7 +187,7 @@ export class AtlasAllocator { // Initiate download gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo); - this.atlases[id.i].download(gl, id); + this.atlases[allocInfo.i].download(gl, allocInfo.allocation); let fence = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0); gl.bindBuffer(gl.PIXEL_PACK_BUFFER, 0); @@ -191,4 +232,30 @@ export class AtlasAllocator { } } } + + canvasSource() { + let useCanvas = (gl, id) => { + let allocInfo = this.#getAllocInfo(id); + let atlas = this.atlases[allocInfo.i]; + + let viewport = atlas.getFramebufferRect(allocInfo.allocation); + + gl.enable(gl.SCISSOR_TEST); + gl.bindFramebuffer(gl.FRAMEBUFFER, atlas.framebuffer); + gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height); + gl.scissor(viewport.x, viewport.y, viewport.width, viewport.height); + + return viewport; + }; + + let resetCanvas = (gl) => { + gl.bindFramebuffer(gl.FRAMEBUFFER, null); + gl.disable(gl.SCISSOR_TEST); + }; + + return { + useCanvas, + resetCanvas, + }; + } } diff --git a/static/haku.js b/static/haku.js index 4f6625c..be99ed7 100644 --- a/static/haku.js +++ b/static/haku.js @@ -1,11 +1,6 @@ let panicImpl; let logImpl, log2Impl; -let canvasBeginImpl, - canvasLineImpl, - canvasRectangleImpl, - canvasCircleImpl, - canvasFillImpl, - canvasStrokeImpl; +let currentBrushRenderer; function allocCheck(p) { if (p == 0) throw new Error("out of memory"); @@ -47,14 +42,8 @@ let [hakuWasm, haku2Wasm] = await Promise.all([ __haku2_log_info: makeLogFunction2("info"), __haku2_log_debug: makeLogFunction2("debug"), - __haku2_canvas_begin: (c) => canvasBeginImpl(c), - __haku2_canvas_line: (c, x1, y1, x2, y2) => canvasLineImpl(c, x1, y1, x2, y2), - __haku2_canvas_rectangle: (c, x, y, width, height) => - canvasRectangleImpl(c, x, y, width, height), - __haku2_canvas_circle: (c, x, y, r) => canvasCircleImpl(c, x, y, r), - __haku2_canvas_fill: (c, r, g, b, a) => canvasFillImpl(c, r, g, b, a), - __haku2_canvas_stroke: (c, r, g, b, a, thickness) => - canvasStrokeImpl(c, r, g, b, a, thickness), + __haku2_canvas_stroke: (c, r, g, b, a, thickness, x1, y1, x2, y2) => + currentBrushRenderer.stroke(c, r, g, b, a, thickness, x1, y1, x2, y2), }, }), ]); @@ -151,13 +140,6 @@ log2Impl = (level, pScope, scopeLen, pMsg, len) => { ); }; -canvasBeginImpl = w.haku_pixmap_begin; -canvasLineImpl = w.haku_pixmap_line; -canvasRectangleImpl = w.haku_pixmap_rectangle; -canvasCircleImpl = w.haku_pixmap_circle; -canvasFillImpl = w.haku_pixmap_fill; -canvasStrokeImpl = w.haku_pixmap_stroke; - w.haku_init_logging(); export const ContKind = { @@ -386,9 +368,12 @@ export class Haku { else return ContKind.Scribble; } - contScribble(pixmap, translationX, translationY) { - w.haku_pixmap_set_translation(pixmap.ptr, translationX, translationY); - let ok = w2.haku2_render(this.#pVm2, pixmap.ptr, this.#renderMaxDepth); + contScribble(renderer, canvas) { + console.assert(currentBrushRenderer == null); + currentBrushRenderer = renderer; + let ok = w2.haku2_render(this.#pVm2, canvas, this.#renderMaxDepth); + currentBrushRenderer = null; + if (!ok) { return this.#exceptionResult(); } else { @@ -415,8 +400,8 @@ export class Haku { while (true) { switch (this.expectedContKind()) { case ContKind.Scribble: - result = await runScribble((pixmap, translationX, translationY) => { - return this.contScribble(pixmap, translationX, translationY); + result = await runScribble((renderer, canvas, translationX, translationY) => { + return this.contScribble(renderer, canvas, translationX, translationY); }); return result; diff --git a/static/index.js b/static/index.js index f968cf8..4b9fe52 100644 --- a/static/index.js +++ b/static/index.js @@ -147,6 +147,7 @@ function readUrl(urlString) { let currentUser = wall.onlineUsers.getUser(session.sessionId); let chunkAllocator = canvasRenderer.atlasAllocator; + let brushRenderer = canvasRenderer.brushRenderer; // Event loop @@ -188,7 +189,7 @@ function readUrl(urlString) { } if (wallEvent.kind.event == "interact") { - user.simulate(wall, wallEvent.kind.interactions); + user.simulate(chunkAllocator, wall, wallEvent.kind.interactions); } } }); @@ -254,7 +255,7 @@ function readUrl(urlString) { let layer = currentUser.getScratchLayer(wall); let result = await currentUser.haku.evalBrush( - selfController(interactionQueue, wall, layer, event), + selfController(interactionQueue, chunkAllocator, brushRenderer, wall, layer, event), ); brushEditor.renderHakuResult(result); }); @@ -263,9 +264,8 @@ function readUrl(urlString) { let scratchLayer = currentUser.commitScratchLayer(wall); if (scratchLayer == null) return; - canvasRenderer.deallocateChunks(scratchLayer); let edits = await scratchLayer.toEdits(); - scratchLayer.destroy(); + scratchLayer.destroy(chunkAllocator); let editRecords = []; let dataParts = []; diff --git a/static/online-users.js b/static/online-users.js index 4467107..ceaf896 100644 --- a/static/online-users.js +++ b/static/online-users.js @@ -42,7 +42,7 @@ export class User { return result; } - simulate(wall, interactions) { + simulate(chunkAllocator, wall, interactions) { console.group("simulate", this.nickname); for (let interaction of interactions) { if (interaction.kind == "setBrush") { @@ -71,6 +71,7 @@ export class User { if (interaction.kind == "scribble" && this.#expectContKind(ContKind.Scribble)) { renderToChunksInArea( + chunkAllocator, this.getScratchLayer(wall), this.simulation.renderArea, (pixmap, translationX, translationY) => { diff --git a/static/painter.js b/static/painter.js index 4bdfa94..b32bd8f 100644 --- a/static/painter.js +++ b/static/painter.js @@ -22,15 +22,21 @@ function* chunksInRectangle(rect, chunkSize) { } } -export function renderToChunksInArea(layer, renderArea, renderToPixmap) { +export function renderToChunksInArea( + chunkAllocator, + brushRenderer, + layer, + renderArea, + renderToCanvas, +) { for (let [chunkX, chunkY] of chunksInRectangle(renderArea, layer.chunkSize)) { - let chunk = layer.getOrCreateChunk(chunkX, chunkY); + let chunk = layer.getOrCreateChunk(chunkAllocator, chunkX, chunkY); if (chunk == null) continue; let translationX = -chunkX * layer.chunkSize; let translationY = -chunkY * layer.chunkSize; - let result = renderToPixmap(chunk.pixmap, translationX, translationY); - chunk.markModified(); + brushRenderer.setTranslation(translationX, translationY); + let result = renderToCanvas(brushRenderer, chunk.id, translationX, translationY); if (result.status != "ok") return result; } @@ -47,14 +53,26 @@ export function dotterRenderArea(wall, dotter) { }; } -export function selfController(interactionQueue, wall, layer, event) { +export function selfController( + interactionQueue, + chunkAllocator, + brushRenderer, + wall, + layer, + event, +) { let renderArea = null; return { - async runScribble(renderToPixmap) { + async runScribble(renderToCanvas) { interactionQueue.push({ kind: "scribble" }); if (renderArea != null) { - let numChunksToRender = numChunksInRectangle(renderArea, layer.chunkSize); - let result = renderToChunksInArea(layer, renderArea, renderToPixmap); + let result = renderToChunksInArea( + chunkAllocator, + brushRenderer, + layer, + renderArea, + renderToCanvas, + ); return result; } else { console.debug("render area is empty, nothing will be rendered"); diff --git a/static/wall.js b/static/wall.js index 2e26764..eba4119 100644 --- a/static/wall.js +++ b/static/wall.js @@ -32,9 +32,9 @@ export class Layer { console.info("created layer", this.id, this.name); } - destroy() { + destroy(chunkAllocator) { for (let { chunk } of this.chunks.values()) { - chunk.destroy(); + chunk.destroy(chunkAllocator); } } diff --git a/static/webgl.js b/static/webgl.js new file mode 100644 index 0000000..f846408 --- /dev/null +++ b/static/webgl.js @@ -0,0 +1,45 @@ +function compileShader(gl, kind, source) { + let shader = gl.createShader(kind); + + gl.shaderSource(shader, source); + gl.compileShader(shader); + + if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { + let error = new Error(`failed to compile shader: ${gl.getShaderInfoLog(shader)}`); + gl.deleteShader(shader); + throw error; + } else { + return shader; + } +} + +export function compileProgram(gl, vertexSource, fragmentSource) { + let vertexShader = compileShader(gl, gl.VERTEX_SHADER, vertexSource); + let fragmentShader = compileShader(gl, gl.FRAGMENT_SHADER, fragmentSource); + + let program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + gl.deleteShader(vertexShader); + gl.deleteShader(fragmentShader); + + if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { + let error = new Error(`failed to link program: ${gl.getProgramInfoLog(program)}`); + gl.deleteProgram(program); + throw error; + } else { + return program; + } +} + +export function orthographicProjection(left, right, top, bottom, near, far) { + // prettier-ignore + return [ + 2 / (right - left), 0, 0, -((right + left) / (right - left)), + 0, 2 / (top - bottom), 0, -((top + bottom) / (top - bottom)), + 0, 0, -2 / (far - near), -((far + near) / (far - near)), + 0, 0, 0, 1, + ]; +}