initial implementation of WebGL-based brush renderer

This commit is contained in:
りき萌 2025-09-05 20:20:45 +02:00
parent b4c3260f49
commit bb55e23979
14 changed files with 385 additions and 247 deletions

View file

@ -77,13 +77,7 @@ pub fn resolve(arity: SystemFnArity, name: &str) -> Option<u8> {
(Nary, "reduce") => 0x95, (Nary, "reduce") => 0x95,
(Nary, "flatten") => 0x96, (Nary, "flatten") => 0x96,
(Nary, "toShape") => 0xc0,
(Nary, "line") => 0xc1,
(Nary, "rect") => 0xc2,
(Nary, "circle") => 0xc3,
(Nary, "stroke") => 0xe0, (Nary, "stroke") => 0xe0,
(Nary, "fill") => 0xe1,
(Nary, "withDotter") => 0xf0, (Nary, "withDotter") => 0xf0,

View file

@ -8,41 +8,21 @@ pub const Canvas = opaque {
if (!status) return error.Draw; if (!status) return error.Draw;
} }
pub fn begin(c: *Canvas) !void { pub fn stroke(c: *Canvas, color: value.Rgba8, thickness: f32, from: value.Vec2, to: value.Vec2) !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 {
const r, const g, const b, const a = color; const r, const g, const b, const a = color;
try wrap(__haku2_canvas_fill(c, r, g, b, a)); try wrap(__haku2_canvas_stroke(c, r, g, b, a, thickness, from[0], from[1], to[0], to[1]));
}
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));
} }
}; };
extern fn __haku2_canvas_begin(c: *Canvas) bool; extern fn __haku2_canvas_stroke(
extern fn __haku2_canvas_line(c: *Canvas, x1: f32, y1: f32, x2: f32, y2: f32) bool; c: *Canvas,
extern fn __haku2_canvas_rectangle(c: *Canvas, x: f32, y: f32, width: f32, height: f32) bool; r: u8,
extern fn __haku2_canvas_circle(c: *Canvas, x: f32, y: f32, r: f32) bool; g: u8,
extern fn __haku2_canvas_fill(c: *Canvas, r: u8, g: u8, b: u8, a: u8) bool; b: u8,
extern fn __haku2_canvas_stroke(c: *Canvas, r: u8, g: u8, b: u8, a: u8, thickness: f32) bool; a: u8,
thickness: f32,
from_x: f32,
from_y: f32,
to_x: f32,
to_y: f32,
) bool;

View file

@ -24,18 +24,13 @@ fn renderRec(vm: *Vm, canvas: *Canvas, val: Value, depth: usize, max_depth: usiz
switch (val.ref.*) { switch (val.ref.*) {
.scribble => { .scribble => {
try canvas.begin(); switch (val.ref.scribble) {
.stroke => |stroke| try canvas.stroke(
switch (val.ref.scribble.shape) { value.rgbaTo8(stroke.color),
.point => |point| try canvas.line(point, point), stroke.thickness,
.line => |line| try canvas.line(line.start, line.end), value.vec2From4(stroke.from),
.rect => |rect| try canvas.rect(rect.top_left, rect.size), value.vec2From4(stroke.to),
.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)),
} }
}, },
@ -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 <shape>)", .{});
},
else => return notAScribble(vm, val), else => return notAScribble(vm, val),
} }
} }

View file

@ -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"); if (val != .ref or val.ref.* != .list) return typeError(cx.vm, val, i, "list");
return val.ref.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 value.Closure => {
const val = cx.args[i]; const val = cx.args[i];
if (val != .ref or val.ref.* != .closure) return typeError(cx.vm, val, i, "function"); 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) }, .{ 0x94, erase("filter", filter) },
.{ 0x95, erase("reduce", reduce) }, .{ 0x95, erase("reduce", reduce) },
.{ 0x96, erase("flatten", flatten) }, .{ 0x96, erase("flatten", flatten) },
.{ 0xc0, erase("toShape", valueToShape) },
.{ 0xc1, erase("line", line) },
.{ 0xc2, erase("rect", rect) },
.{ 0xc3, erase("circle", circle) },
.{ 0xe0, erase("stroke", stroke) }, .{ 0xe0, erase("stroke", stroke) },
.{ 0xe1, erase("fill", fill) },
.{ 0xf0, erase("withDotter", withDotter) }, .{ 0xf0, erase("withDotter", withDotter) },
}); });
@ -738,51 +725,14 @@ fn flatten(list: value.List, cx: Context) Vm.Error!value.Ref {
return .{ .list = flattened_list }; return .{ .list = flattened_list };
} }
fn toShape(val: value.Value) ?value.Shape { fn stroke(thickness: f32, color: Rgba, from: Vec4, to: Vec4) value.Ref {
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 {
return .{ .scribble = .{ return .{ .scribble = .{
.shape = shape, .stroke = .{
.action = .{ .stroke = .{
.thickness = thickness, .thickness = thickness,
.color = color.value, .color = color.value,
} }, .from = from.value,
.to = to.value,
},
} }; } };
} }

View file

@ -62,7 +62,6 @@ pub const Value = union(enum) {
.ref => |r| switch (r.*) { .ref => |r| switch (r.*) {
.closure => "function", .closure => "function",
.list => "list", .list => "list",
.shape => "shape",
.scribble => "scribble", .scribble => "scribble",
.reticle => "reticle", .reticle => "reticle",
}, },
@ -86,7 +85,7 @@ pub const Value = union(enum) {
} }
try writer.writeAll("]"); 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) { pub const Ref = union(enum) {
closure: Closure, closure: Closure,
list: List, list: List,
shape: Shape,
scribble: Scribble, scribble: Scribble,
reticle: Reticle, reticle: Reticle,
}; };
@ -161,44 +159,14 @@ pub const Closure = struct {
pub const List = []Value; pub const List = []Value;
pub const Shape = union(enum) { pub const Scribble = union(enum) {
point: Vec2,
line: Line,
rect: Rect,
circle: Circle,
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, stroke: Stroke,
fill: Fill,
pub const Stroke = struct { pub const Stroke = struct {
thickness: f32, thickness: f32,
color: Rgba, color: Rgba,
}; from: Vec4,
to: Vec4,
pub const Fill = struct {
color: Rgba,
};
}; };
}; };

168
static/brush-renderer.js Normal file
View file

@ -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;
}
}

View file

@ -2,6 +2,8 @@ import { listen, Pool } from "rkgk/framework.js";
import { Viewport } from "rkgk/viewport.js"; import { Viewport } from "rkgk/viewport.js";
import { Wall, chunkKey } from "rkgk/wall.js"; import { Wall, chunkKey } from "rkgk/wall.js";
import { AtlasAllocator } from "rkgk/chunk-allocator.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 { class CanvasRenderer extends HTMLElement {
viewport = new Viewport(); viewport = new Viewport();
@ -74,7 +76,7 @@ class CanvasRenderer extends HTMLElement {
// Renderer initialization // Renderer initialization
#initializeRenderer() { #initializeRenderer() {
console.groupCollapsed("initializeRenderer"); console.group("initializeRenderer");
console.info("vendor", this.gl.getParameter(this.gl.VENDOR)); console.info("vendor", this.gl.getParameter(this.gl.VENDOR));
console.info("renderer", this.gl.getParameter(this.gl.RENDERER)); 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 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. // we store (8 * 8 = 64) chunks per texture atlas, so we can't batch more than that.
const maxRects = 64; const maxRects = 64;
let renderChunksProgramId = this.#compileProgram( let renderChunksProgramId = compileProgram(
this.gl,
// Vertex // Vertex
`#version 300 es `#version 300 es
@ -200,47 +204,13 @@ class CanvasRenderer extends HTMLElement {
this.batches = []; this.batches = [];
this.batchPool = new Pool(); this.batchPool = new Pool();
this.brushRenderer = new BrushRenderer(this.gl, this.atlasAllocator.canvasSource());
console.debug("GL error state", this.gl.getError()); console.debug("GL error state", this.gl.getError());
console.groupEnd(); 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 // Renderer
#render() { #render() {
@ -256,6 +226,7 @@ class CanvasRenderer extends HTMLElement {
} }
this.gl.viewport(0, 0, this.canvas.width, this.canvas.height); 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.clearColor(1, 1, 1, 1);
this.gl.clear(this.gl.COLOR_BUFFER_BIT); this.gl.clear(this.gl.COLOR_BUFFER_BIT);
@ -303,8 +274,9 @@ class CanvasRenderer extends HTMLElement {
this.#resetRectBuffer(); this.#resetRectBuffer();
for (let chunk of chunks) { for (let chunk of chunks) {
let { i, allocation } = chunk.allocation; let atlasIndex = this.atlasAllocator.getAtlasIndex(chunk.id);
let atlas = this.atlasAllocator.atlases[i]; let allocation = this.atlasAllocator.getAllocation(chunk.id);
let atlas = this.atlasAllocator.atlases[atlasIndex];
this.#pushRect( this.#pushRect(
chunk.x * this.wall.chunkSize, chunk.x * this.wall.chunkSize,
chunk.y * this.wall.chunkSize, chunk.y * this.wall.chunkSize,
@ -358,15 +330,14 @@ class CanvasRenderer extends HTMLElement {
for (let chunkX = left; chunkX < right; ++chunkX) { for (let chunkX = left; chunkX < right; ++chunkX) {
let chunk = layer.getChunk(chunkX, chunkY); let chunk = layer.getChunk(chunkX, chunkY);
if (chunk != null) { if (chunk != null) {
let allocation = chunk.id; let atlasIndex = this.atlasAllocator.getAtlasIndex(chunk.id);
let array = batch.get(atlasIndex);
let array = batch.get(allocation.i);
if (array == null) { if (array == null) {
array = []; 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 });
} }
} }
} }

View file

@ -1,7 +1,7 @@
class Atlas { class Atlas {
static getInitBuffer(chunkSize, nChunks) { static getInitBuffer(chunkSize, nChunks) {
let imageSize = 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) { constructor(gl, chunkSize, nChunks, initBuffer) {
@ -44,8 +44,12 @@ class Atlas {
gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.bindFramebuffer(gl.FRAMEBUFFER, null);
} }
alloc() { alloc(gl, initBuffer) {
return this.free.pop(); let xy = this.free.pop();
if (xy != null) {
this.upload(gl, xy, initBuffer);
}
return xy;
} }
dealloc(xy) { dealloc(xy) {
@ -81,11 +85,24 @@ class Atlas {
); );
gl.bindFramebuffer(gl.FRAMEBUFFER, null); 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 { export class AtlasAllocator {
atlases = []; atlases = [];
// Allocation names
#ids = new Map();
#idCounter = 1;
// Download buffers // Download buffers
#pboPool = []; #pboPool = [];
#downloadBufferPool = []; #downloadBufferPool = [];
@ -98,6 +115,28 @@ export class AtlasAllocator {
this.initBuffer = Atlas.getInitBuffer(chunkSize, nChunks); 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() { alloc() {
// Right now we do a dumb linear scan through all atlases, but in the future it would be // 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 // 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) { for (let i = 0; i < this.atlases.length; ++i) {
let atlas = this.atlases[i]; let atlas = this.atlases[i];
let allocation = atlas.alloc(); let allocation = atlas.alloc(this.gl, this.initBuffer);
if (allocation != null) { if (allocation != null) {
return { i, allocation }; return this.#obtainId({ i, allocation });
} }
} }
let i = this.atlases.length; let i = this.atlases.length;
let atlas = new Atlas(this.gl, this.chunkSize, this.nChunks, this.initBuffer); 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); this.atlases.push(atlas);
return { i, allocation }; return this.#obtainId({ i, allocation });
} }
dealloc(id) { dealloc(id) {
let { i, allocation } = id; let { i, allocation } = this.#getAllocInfo(id);
let atlas = this.atlases[i]; let atlas = this.atlases[i];
atlas.dealloc(allocation); atlas.dealloc(allocation);
this.#releaseId(id);
} }
upload(id, source) { upload(id, source) {
let { i, allocation } = id; let { i, allocation } = this.#getAllocInfo(id);
this.atlases[i].upload(this.gl, allocation, source); this.atlases[i].upload(this.gl, allocation, source);
} }
async download(id) { async download(id) {
let gl = this.gl; let gl = this.gl;
let allocInfo = this.#getAllocInfo(id);
// Get PBO // Get PBO
@ -146,7 +187,7 @@ export class AtlasAllocator {
// Initiate download // Initiate download
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, pbo); 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); let fence = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, 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,
};
}
} }

View file

@ -1,11 +1,6 @@
let panicImpl; let panicImpl;
let logImpl, log2Impl; let logImpl, log2Impl;
let canvasBeginImpl, let currentBrushRenderer;
canvasLineImpl,
canvasRectangleImpl,
canvasCircleImpl,
canvasFillImpl,
canvasStrokeImpl;
function allocCheck(p) { function allocCheck(p) {
if (p == 0) throw new Error("out of memory"); 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_info: makeLogFunction2("info"),
__haku2_log_debug: makeLogFunction2("debug"), __haku2_log_debug: makeLogFunction2("debug"),
__haku2_canvas_begin: (c) => canvasBeginImpl(c), __haku2_canvas_stroke: (c, r, g, b, a, thickness, x1, y1, x2, y2) =>
__haku2_canvas_line: (c, x1, y1, x2, y2) => canvasLineImpl(c, x1, y1, x2, y2), currentBrushRenderer.stroke(c, r, g, b, a, thickness, 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),
}, },
}), }),
]); ]);
@ -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(); w.haku_init_logging();
export const ContKind = { export const ContKind = {
@ -386,9 +368,12 @@ export class Haku {
else return ContKind.Scribble; else return ContKind.Scribble;
} }
contScribble(pixmap, translationX, translationY) { contScribble(renderer, canvas) {
w.haku_pixmap_set_translation(pixmap.ptr, translationX, translationY); console.assert(currentBrushRenderer == null);
let ok = w2.haku2_render(this.#pVm2, pixmap.ptr, this.#renderMaxDepth); currentBrushRenderer = renderer;
let ok = w2.haku2_render(this.#pVm2, canvas, this.#renderMaxDepth);
currentBrushRenderer = null;
if (!ok) { if (!ok) {
return this.#exceptionResult(); return this.#exceptionResult();
} else { } else {
@ -415,8 +400,8 @@ export class Haku {
while (true) { while (true) {
switch (this.expectedContKind()) { switch (this.expectedContKind()) {
case ContKind.Scribble: case ContKind.Scribble:
result = await runScribble((pixmap, translationX, translationY) => { result = await runScribble((renderer, canvas, translationX, translationY) => {
return this.contScribble(pixmap, translationX, translationY); return this.contScribble(renderer, canvas, translationX, translationY);
}); });
return result; return result;

View file

@ -147,6 +147,7 @@ function readUrl(urlString) {
let currentUser = wall.onlineUsers.getUser(session.sessionId); let currentUser = wall.onlineUsers.getUser(session.sessionId);
let chunkAllocator = canvasRenderer.atlasAllocator; let chunkAllocator = canvasRenderer.atlasAllocator;
let brushRenderer = canvasRenderer.brushRenderer;
// Event loop // Event loop
@ -188,7 +189,7 @@ function readUrl(urlString) {
} }
if (wallEvent.kind.event == "interact") { 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 layer = currentUser.getScratchLayer(wall);
let result = await currentUser.haku.evalBrush( let result = await currentUser.haku.evalBrush(
selfController(interactionQueue, wall, layer, event), selfController(interactionQueue, chunkAllocator, brushRenderer, wall, layer, event),
); );
brushEditor.renderHakuResult(result); brushEditor.renderHakuResult(result);
}); });
@ -263,9 +264,8 @@ function readUrl(urlString) {
let scratchLayer = currentUser.commitScratchLayer(wall); let scratchLayer = currentUser.commitScratchLayer(wall);
if (scratchLayer == null) return; if (scratchLayer == null) return;
canvasRenderer.deallocateChunks(scratchLayer);
let edits = await scratchLayer.toEdits(); let edits = await scratchLayer.toEdits();
scratchLayer.destroy(); scratchLayer.destroy(chunkAllocator);
let editRecords = []; let editRecords = [];
let dataParts = []; let dataParts = [];

View file

@ -42,7 +42,7 @@ export class User {
return result; return result;
} }
simulate(wall, interactions) { simulate(chunkAllocator, wall, interactions) {
console.group("simulate", this.nickname); console.group("simulate", this.nickname);
for (let interaction of interactions) { for (let interaction of interactions) {
if (interaction.kind == "setBrush") { if (interaction.kind == "setBrush") {
@ -71,6 +71,7 @@ export class User {
if (interaction.kind == "scribble" && this.#expectContKind(ContKind.Scribble)) { if (interaction.kind == "scribble" && this.#expectContKind(ContKind.Scribble)) {
renderToChunksInArea( renderToChunksInArea(
chunkAllocator,
this.getScratchLayer(wall), this.getScratchLayer(wall),
this.simulation.renderArea, this.simulation.renderArea,
(pixmap, translationX, translationY) => { (pixmap, translationX, translationY) => {

View file

@ -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)) { 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; if (chunk == null) continue;
let translationX = -chunkX * layer.chunkSize; let translationX = -chunkX * layer.chunkSize;
let translationY = -chunkY * layer.chunkSize; let translationY = -chunkY * layer.chunkSize;
let result = renderToPixmap(chunk.pixmap, translationX, translationY); brushRenderer.setTranslation(translationX, translationY);
chunk.markModified(); let result = renderToCanvas(brushRenderer, chunk.id, translationX, translationY);
if (result.status != "ok") return result; 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; let renderArea = null;
return { return {
async runScribble(renderToPixmap) { async runScribble(renderToCanvas) {
interactionQueue.push({ kind: "scribble" }); interactionQueue.push({ kind: "scribble" });
if (renderArea != null) { if (renderArea != null) {
let numChunksToRender = numChunksInRectangle(renderArea, layer.chunkSize); let result = renderToChunksInArea(
let result = renderToChunksInArea(layer, renderArea, renderToPixmap); chunkAllocator,
brushRenderer,
layer,
renderArea,
renderToCanvas,
);
return result; return result;
} else { } else {
console.debug("render area is empty, nothing will be rendered"); console.debug("render area is empty, nothing will be rendered");

View file

@ -32,9 +32,9 @@ export class Layer {
console.info("created layer", this.id, this.name); console.info("created layer", this.id, this.name);
} }
destroy() { destroy(chunkAllocator) {
for (let { chunk } of this.chunks.values()) { for (let { chunk } of this.chunks.values()) {
chunk.destroy(); chunk.destroy(chunkAllocator);
} }
} }

45
static/webgl.js Normal file
View file

@ -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,
];
}