import { getPath } from "./common.js"; import { makeSliders } from "./fontSliders.js"; import { updateSliders } from "./slider.js"; class FontRenderer { createBuffers(device) { this.frames = []; ////////////////////////////////////////////////////////////////////// // glyph ////////////////////////////////////////////////////////////////////// const glyphBufferSize = this.exports.font_layout__glyph_buffer_size(this.fontLayoutAddress); const glyphBuffer = device.createBuffer({ label: `glyph buffer`, size: glyphBufferSize, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); const glyphBufferAddress = this.exports.font_layout__glyph_buffer_address(this.fontLayoutAddress); device.queue.writeBuffer(glyphBuffer, 0, this.module.memory.buffer, glyphBufferAddress, glyphBufferSize); ////////////////////////////////////////////////////////////////////// // texture ////////////////////////////////////////////////////////////////////// const textureWidth = this.exports.font_layout__texture_width(this.fontLayoutAddress); const textureHeight = this.exports.font_layout__texture_height(this.fontLayoutAddress); const textureAddress = this.exports.font_layout__texture_address(this.fontLayoutAddress); //console.log(textureWidth, textureHeight, textureAddress); const texture = device.createTexture({ size: [textureWidth, textureHeight], format: 'r8unorm', usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST, }); device.queue.writeTexture({ texture: texture }, this.module.memory.buffer, { offset: textureAddress, bytesPerRow: textureWidth }, { width: textureWidth, height: textureHeight }); ////////////////////////////////////////////////////////////////////// // sampler ////////////////////////////////////////////////////////////////////// const sampler = device.createSampler({ magFilter: "linear", minFilter: "linear", }); ////////////////////////////////////////////////////////////////////// // bind group ////////////////////////////////////////////////////////////////////// this.perFontBindGroup = device.createBindGroup({ label: `font per-font bind group`, layout: this.bindGroupLayout.perFont, entries: [{ binding: 0, resource: sampler, }, { binding: 1, resource: texture.createView(), }, { binding: 2, resource: { buffer: glyphBuffer }, }] }); ////////////////////////////////////////////////////////////////////// // per-frame (layout, configuration) ////////////////////////////////////////////////////////////////////// this.layoutBufferAddress = this.exports.font_layout__layout_buffer_address(this.fontLayoutAddress); const layoutBufferSize = this.exports.font_layout__layout_buffer_size(this.fontLayoutAddress); for (let i = 0; i < 2; i++) { const layoutBuffer = device.createBuffer({ label: `layout buffer ${i}`, size: layoutBufferSize, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST, }); const configurationBuffer = device.createBuffer({ label: `configuration buffer ${i}`, size: this.sliderArray.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); const bindGroup = device.createBindGroup({ label: `font per-frame bind group ${i}`, layout: this.bindGroupLayout.perFrame, entries: [{ binding: 0, resource: { buffer: layoutBuffer }, }, { binding: 1, resource: { buffer: configurationBuffer }, }] }); this.frames.push({ layoutBuffer: layoutBuffer, configurationBuffer: configurationBuffer, bindGroup: bindGroup, }); } } font_layout__draw(s) { const length = Math.min(s.length, this.maxStringLength - 1); for (let i = 0; i < length; i++) { const c = s.charCodeAt(i) % 256; this.stringBuffer[i] = c; } this.stringBuffer[length] = 0; this.exports.font_layout__draw(this.fontLayoutAddress, this.stringBufferAddress); } constructor(device, canvasFormat, viewUniformBuffer, wgsl, module, fontBufferSrc) { const label = "font"; this.module = module; this.exports = module.instance.exports; ////////////////////////////////////////////////////////////////////// // font layout ////////////////////////////////////////////////////////////////////// this.fontBufferAddress = this.exports.mem_alloc(fontBufferSrc.byteLength); const fontBufferDst = new Uint8Array(this.module.memory.buffer, this.fontBufferAddress, fontBufferSrc.byteLength); fontBufferDst.set(new Uint8Array(fontBufferSrc)); this.maxGlyphs = 1024; this.fontLayoutAddress = this.exports.font_layout__create(this.fontBufferAddress, this.maxGlyphs); // text buffer this.maxStringLength = 1024; this.stringBufferAddress = this.exports.mem_alloc(this.maxStringLength); this.stringBuffer = new Uint8Array(this.module.memory.buffer, this.stringBufferAddress); this.lastString = undefined; ////////////////////////////////////////////////////////////////////// // shader module ////////////////////////////////////////////////////////////////////// const shaderModule = device.createShaderModule({ label: `${label} shader`, code: wgsl, }); ////////////////////////////////////////////////////////////////////// // index buffer ////////////////////////////////////////////////////////////////////// const array = new Uint16Array(6); array[0] = 0; array[1] = 1; array[2] = 2; array[3] = 0; array[4] = 3; array[5] = 1; this.buffer = device.createBuffer({ label: `${label} renderer buffer`, size: array.byteLength, usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST, }); device.queue.writeBuffer(this.buffer, 0, array, 0, array.length); ////////////////////////////////////////////////////////////////////// // pipeline ////////////////////////////////////////////////////////////////////// this.bindGroupLayout = { view: device.createBindGroupLayout({ label: `${label} bind group layout view`, entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "uniform" } }] }), perFont: device.createBindGroupLayout({ label: `${label} bind group per-font`, entries: [ { // sampler binding: 0, visibility: GPUShaderStage.FRAGMENT, sampler: {} }, { // texture binding: 1, visibility: GPUShaderStage.FRAGMENT, texture: {}, }, { // glyph buffer binding: 2, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "read-only-storage" } } ] }), perFrame: device.createBindGroupLayout({ label: `${label} bind group per-frame`, entries: [{ // layout buffer binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: "read-only-storage" } }, { // configuration binding: 1, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: "uniform" } }] }), }; const pipelineLayout = device.createPipelineLayout({ label: `${label} pipeline layout`, bindGroupLayouts: [ this.bindGroupLayout.view, this.bindGroupLayout.perFont, this.bindGroupLayout.perFrame, ], }); this.renderPipelines = {} for (let i = 1; i <= 4; i += 3) { this.renderPipelines[i] = device.createRenderPipeline({ label: `${label} pipeline sample count ${i}`, layout: pipelineLayout, vertex: { module: shaderModule, entryPoint: "vertexMain", }, fragment: { module: shaderModule, entryPoint: "fragmentMain", targets: [{ blend: { color: { dstFactor: "one-minus-src-alpha", srcFactor: "src-alpha", }, alpha: { dstFactor: "one-minus-src-alpha", srcFactor: "src-alpha", }, operation: "add", }, format: canvasFormat, }] }, primitive: { topology: "triangle-list", }, depthStencil: { depthWriteEnabled: true, depthCompare: "always", format: "depth24plus", }, multisample: { count: i, } }); } ////////////////////////////////////////////////////////////////////// // sliders ////////////////////////////////////////////////////////////////////// this.sliders = makeSliders(); this.sliderArray = new Float32Array(4 * 6); ////////////////////////////////////////////////////////////////////// // buffers ////////////////////////////////////////////////////////////////////// this.createBuffers(device); ////////////////////////////////////////////////////////////////////// // bind group ////////////////////////////////////////////////////////////////////// this.viewBindGroup = device.createBindGroup({ label: `${label} bind group`, layout: this.bindGroupLayout.view, entries: [{ binding: 0, resource: { buffer: viewUniformBuffer }, }], }); } update(device, frameNumber) { const configuration = updateSliders(this.sliders); ////////////////////////////////////////////////////////////////////// // string draw ////////////////////////////////////////////////////////////////////// if (this.lastString === undefined || this.lastString !== configuration.text) { this.lastString = configuration.text; this.font_layout__draw(configuration.text); } ////////////////////////////////////////////////////////////////////// // layout buffer ////////////////////////////////////////////////////////////////////// this.instanceCount = this.exports.font_layout__layout_buffer_index(this.fontLayoutAddress); const layoutBufferSize = this.instanceCount * 4 * 4; device.queue.writeBuffer(this.frames[frameNumber].layoutBuffer, 0, this.module.memory.buffer, this.layoutBufferAddress, layoutBufferSize); ////////////////////////////////////////////////////////////////////// // sliders ////////////////////////////////////////////////////////////////////// this.sliderArray[0] = configuration.mode; this.sliderArray[1] = configuration.scale; this.sliderArray[2] = configuration.translateX; this.sliderArray[3] = configuration.translateY; this.sliderArray[4] = configuration.distanceThreshold; this.sliderArray[5] = configuration.distanceScale; this.sliderArray[6] = configuration.aliasingEnable; this.sliderArray[7] = configuration.rampMin; this.sliderArray[8] = configuration.subpixelEnable; this.sliderArray[9] = configuration.subpixelStrength; this.sliderArray[10] = configuration.subpixelOffset; this.sliderArray[11] = configuration.rampMax; this.sliderArray[12] = configuration.supersampleCount; this.sliderArray[13] = configuration.supersampleOffset; this.sliderArray[14] = configuration.outlineThreshold; this.sliderArray[15] = configuration.outlineScale; this.sliderArray[16] = configuration.insideColor[0] / 255.0; this.sliderArray[17] = configuration.insideColor[1] / 255.0; this.sliderArray[18] = configuration.insideColor[2] / 255.0; this.sliderArray[20] = configuration.outlineColor[0] / 255.0; this.sliderArray[21] = configuration.outlineColor[1] / 255.0; this.sliderArray[22] = configuration.outlineColor[2] / 255.0; device.queue.writeBuffer(this.frames[frameNumber].configurationBuffer, 0, this.sliderArray); return configuration; } render(renderPass, frameNumber, sampleCount) { renderPass.setPipeline(this.renderPipelines[sampleCount]); renderPass.setIndexBuffer(this.buffer, "uint16"); renderPass.setBindGroup(0, this.viewBindGroup); renderPass.setBindGroup(1, this.perFontBindGroup); renderPass.setBindGroup(2, this.frames[frameNumber].bindGroup); for (let i = 0; i < this.instanceCount; i++) { renderPass.drawIndexed(6, 1, 0, 0, i); } } } async function loadFont(device, canvasFormat, viewUniformBuffer, module) { const fontWgsl = await getPath("font.wgsl"); const liberationResponse = await fetch("liberation.data"); const liberationBuffer = await liberationResponse.arrayBuffer(); const renderer = new FontRenderer(device, canvasFormat, viewUniformBuffer, fontWgsl, module, liberationBuffer); return renderer; } export { loadFont };