This commit is contained in:
Andras Schmelczer 2026-05-21 07:43:10 +01:00
commit 6bc125be1c
104 changed files with 3084 additions and 2410 deletions

View file

@ -7,6 +7,12 @@ import {
} from '../../utils/graphics/cached-buffer-write';
import { smartCompile } from '../../utils/graphics/smart-compile';
import { CommonState } from '../common-state/common-state';
import {
LINE_SEGMENT_VERTEX_BUFFER_LAYOUT,
LINE_SEGMENT_VERTICES,
LineSegmentBuffer,
} from '../common/line-segment-buffer';
import lineSegmentShader from '../common/line-segment.wgsl?raw';
import shader from './brush.wgsl?raw';
export interface BrushSettings {
@ -20,12 +26,7 @@ export interface BrushSettings {
brushGrainMaxStrength: number;
}
interface LineSegment {
from: vec2;
to: vec2;
}
interface BrushParameterSettings extends BrushSettings {
interface BrushParameters extends BrushSettings {
pixelRatio?: number;
selectedColorIndex: number;
}
@ -35,6 +36,8 @@ export const getSafePixelRatio = (pixelRatio: number | undefined): number =>
? pixelRatio
: 1;
const UNIFORM_COUNT = 16;
const setBrushUniformValues = (
target: Float32Array,
{
@ -48,15 +51,14 @@ const setBrushUniformValues = (
brushGrainMaxStrength,
selectedColorIndex,
pixelRatio,
}: BrushParameterSettings
}: BrushParameters
): void => {
const safePixelRatio = getSafePixelRatio(pixelRatio);
const brushRadius = (brushSize * safePixelRatio) / 2;
target[0] = brushRadius;
target[1] = brushRadius * brushRadius;
target[2] = 0;
target[3] = 0;
// target[2], target[3] are WGSL alignment padding for brushValue:vec4 — never read by the shader.
target[4] = selectedColorIndex === 0 ? 1 : 0;
target[5] = selectedColorIndex === 1 ? 1 : 0;
target[6] = selectedColorIndex === 2 ? 1 : 0;
@ -70,78 +72,81 @@ const setBrushUniformValues = (
};
export class BrushPipeline {
private static readonly UNIFORM_COUNT = 16;
private static readonly MAX_LINE_COUNT = appConfig.pipelines.brush.maxLineCount;
private static readonly VERTICES_PER_LINE_SEGMENT = 6;
private static readonly ATTRIBUTES_PER_LINE_SEGMENT = 4;
private readonly bindGroupLayout: GPUBindGroupLayout;
private readonly bindGroup: GPUBindGroup;
private readonly multiTargetPipeline: GPURenderPipeline;
private readonly renderPipeline: GPURenderPipeline;
private readonly uniforms: GPUBuffer;
private readonly uniformValues = new Float32Array(BrushPipeline.UNIFORM_COUNT);
private readonly uniformCache = createCachedFloat32BufferWrite(
BrushPipeline.UNIFORM_COUNT
);
private readonly vertexBuffer: GPUBuffer;
private readonly vertexUploadData = new Float32Array(
BrushPipeline.MAX_LINE_COUNT *
BrushPipeline.VERTICES_PER_LINE_SEGMENT *
BrushPipeline.ATTRIBUTES_PER_LINE_SEGMENT
);
private lineSegments: Array<LineSegment> = [];
private actualSegments: Array<LineSegment> = [];
private readonly uniformValues = new Float32Array(UNIFORM_COUNT);
private readonly uniformCache = createCachedFloat32BufferWrite(UNIFORM_COUNT);
private readonly segments: LineSegmentBuffer;
public constructor(
private readonly device: GPUDevice,
private readonly commonState: CommonState
) {
this.bindGroupLayout = device.createBindGroupLayout(BrushPipeline.bindGroupLayout);
this.segments = new LineSegmentBuffer(device, appConfig.pipelines.brush.maxLineCount);
this.vertexBuffer = device.createBuffer({
size:
BrushPipeline.MAX_LINE_COUNT *
BrushPipeline.VERTICES_PER_LINE_SEGMENT *
BrushPipeline.ATTRIBUTES_PER_LINE_SEGMENT *
Float32Array.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
const shaderModule = smartCompile(device, CommonState.shaderCode, shader);
this.multiTargetPipeline = this.createPipeline(shaderModule, 'fragmentMrt', 1);
this.uniforms = this.device.createBuffer({
size: BrushPipeline.UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
this.bindGroup = this.device.createBindGroup({
layout: this.bindGroupLayout,
this.bindGroupLayout = device.createBindGroupLayout({
entries: [
{
binding: 0,
resource: {
buffer: this.uniforms,
},
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
buffer: { type: 'uniform' },
},
],
});
}
public addSwipeSegment(from: vec2, to: vec2) {
this.lineSegments.push({
from: vec2.clone(from),
to: vec2.clone(to),
const shaderModule = smartCompile(
device,
CommonState.shaderCode,
lineSegmentShader,
shader
);
this.renderPipeline = device.createRenderPipeline({
layout: device.createPipelineLayout({
bindGroupLayouts: [this.commonState.bindGroupLayout, this.bindGroupLayout],
}),
vertex: {
module: shaderModule,
entryPoint: 'vertex',
buffers: [LINE_SEGMENT_VERTEX_BUFFER_LAYOUT],
},
fragment: {
module: shaderModule,
entryPoint: 'fragmentMrt',
targets: [
{
format: 'rgba16float',
blend: {
color: { operation: 'max', srcFactor: 'one', dstFactor: 'one' },
alpha: { operation: 'max', srcFactor: 'one', dstFactor: 'one' },
},
},
],
},
primitive: { topology: 'triangle-list' },
});
this.uniforms = device.createBuffer({
size: UNIFORM_COUNT * Float32Array.BYTES_PER_ELEMENT,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
this.bindGroup = device.createBindGroup({
layout: this.bindGroupLayout,
entries: [{ binding: 0, resource: { buffer: this.uniforms } }],
});
}
public clearSwipes() {
this.lineSegments.length = 0;
this.actualSegments.length = 0;
public addSwipeSegment(from: vec2, to: vec2): void {
this.segments.add(from, to);
}
public setParameters(parameters: BrushParameterSettings) {
public clearSwipes(): void {
this.segments.clear();
}
public setParameters(parameters: BrushParameters): void {
setBrushUniformValues(this.uniformValues, parameters);
writeFloat32BufferIfChanged(
this.device,
@ -149,188 +154,34 @@ export class BrushPipeline {
this.uniformValues,
this.uniformCache
);
this.actualSegments = this.lineSegments.slice();
this.lineSegments.length = 0;
if (this.actualSegments.length === 0) {
return;
}
if (this.actualSegments.length > BrushPipeline.MAX_LINE_COUNT) {
this.actualSegments = BrushPipeline.subsampleSegments(this.actualSegments);
}
const lineCount = this.lineCount;
let floatOffset = 0;
for (let i = 0; i < lineCount; i++) {
const segment = this.actualSegments[i];
floatOffset = this.writeSegmentVertices(
this.vertexUploadData,
floatOffset,
segment.from,
segment.to
);
}
this.device.queue.writeBuffer(
this.vertexBuffer,
0,
this.vertexUploadData,
0,
floatOffset
);
}
private static subsampleSegments(segments: Array<LineSegment>): Array<LineSegment> {
if (segments.length <= BrushPipeline.MAX_LINE_COUNT) {
return segments;
}
const result: Array<LineSegment> = [];
for (let i = 0; i < BrushPipeline.MAX_LINE_COUNT; i++) {
const index = Math.round(
(i * (segments.length - 1)) / (BrushPipeline.MAX_LINE_COUNT - 1)
);
result.push(segments[index]);
}
return result;
}
private writeSegmentVertices(
target: Float32Array,
offset: number,
from: vec2,
to: vec2
): number {
target[offset++] = from[0];
target[offset++] = from[1];
target[offset++] = to[0];
target[offset++] = to[1];
return offset;
this.segments.flush();
}
public executeMultiTarget(
commandEncoder: GPUCommandEncoder,
sourceMapOut: GPUTextureView
sourceMapOut: GPUTextureView,
timestampWrites?: GPURenderPassTimestampWrites
): boolean {
return this.executeWithPipeline(commandEncoder, this.multiTargetPipeline, [
sourceMapOut,
]);
}
private executeWithPipeline(
commandEncoder: GPUCommandEncoder,
pipeline: GPURenderPipeline,
textureViews: Array<GPUTextureView>
): boolean {
if (this.lineCount === 0) {
const lineCount = this.segments.activeCount;
if (lineCount === 0) {
return false;
}
const renderPassDescriptor: GPURenderPassDescriptor = {
colorAttachments: textureViews.map<GPURenderPassColorAttachment>((view) => ({
view,
loadOp: 'load',
storeOp: 'store',
})),
};
const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor);
passEncoder.setPipeline(pipeline);
const passEncoder = commandEncoder.beginRenderPass({
colorAttachments: [{ view: sourceMapOut, loadOp: 'load', storeOp: 'store' }],
timestampWrites,
});
passEncoder.setPipeline(this.renderPipeline);
this.commonState.execute(passEncoder);
passEncoder.setBindGroup(1, this.bindGroup);
passEncoder.setVertexBuffer(0, this.vertexBuffer);
passEncoder.draw(BrushPipeline.VERTICES_PER_LINE_SEGMENT, this.lineCount);
passEncoder.setVertexBuffer(0, this.segments.vertexBuffer);
passEncoder.draw(LINE_SEGMENT_VERTICES, lineCount);
passEncoder.end();
return true;
}
public destroy() {
this.vertexBuffer.destroy();
public destroy(): void {
this.segments.destroy();
this.uniforms.destroy();
}
private createPipeline(
shaderModule: GPUShaderModule,
fragmentEntryPoint: string,
colorTargetCount: number
): GPURenderPipeline {
return this.device.createRenderPipeline({
layout: this.device.createPipelineLayout({
bindGroupLayouts: [this.commonState.bindGroupLayout, this.bindGroupLayout],
}),
vertex: {
module: shaderModule,
entryPoint: 'vertex',
buffers: [
{
arrayStride:
Float32Array.BYTES_PER_ELEMENT * BrushPipeline.ATTRIBUTES_PER_LINE_SEGMENT,
stepMode: 'instance',
attributes: [
{
shaderLocation: 0,
format: 'float32x2',
offset: 0,
},
{
shaderLocation: 1,
format: 'float32x2',
offset: Float32Array.BYTES_PER_ELEMENT * 2,
},
],
},
],
},
fragment: {
module: shaderModule,
entryPoint: fragmentEntryPoint,
targets: Array.from(
{ length: colorTargetCount },
() => BrushPipeline.colorTarget
),
},
primitive: {
topology: 'triangle-list',
},
});
}
private static get colorTarget(): GPUColorTargetState {
return {
format: 'rgba16float',
blend: {
color: {
operation: 'max',
srcFactor: 'one',
dstFactor: 'one',
},
alpha: {
operation: 'max',
srcFactor: 'one',
dstFactor: 'one',
},
},
};
}
private static get bindGroupLayout(): GPUBindGroupLayoutDescriptor {
return {
entries: [
{
binding: 0,
visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT,
buffer: {
type: 'uniform',
},
},
],
};
}
private get lineCount() {
return this.actualSegments.length;
}
}