diff --git a/bun.lock b/bun.lock index 32525e0d3..943ca0743 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,7 @@ "version": "0.1.7", "dependencies": { "jimp": "1.6.0", + "typegpu": "^0.7.1", "yoga-layout": "3.2.1", }, "devDependencies": { @@ -199,6 +200,18 @@ "@opentui/core": ["@opentui/core@workspace:packages/core"], + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LdEcuzG9I9jrz2snybqh4WAaEJY3oRbv1eh4fLaiWfr/P9YiSbsN1d30xJY0WUAe0vKvcj1yp/3SlyiyFqqzOg=="], + + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-YuSpHznKbNJAbh3C6ghI/B+w3vNcbJWsgAeI+9BZ2bQfb4U4ThWfj2Wzfayek8VDYkkKJdpjqKEMFT5RD8Gosw=="], + + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-djjd8RnDeKKyFIf1Sig7e0JnKq3qwRIDC1KFKQiR49spOGgCe9eeXxOWlNmx062WEslUk0Ic9FRPkpC8+04Kzw=="], + + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.7", "", { "os": "linux", "cpu": "x64" }, "sha512-8e8x3Lu/vcgIvWKqxWlV9qirkO1krNdIoct1doNNZNVOG2R/OzR/07zNf5H0gTkpjpL4R/TozLDC9jPTSEuLVw=="], + + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-2Vnrj2RPH7m6wI2VwBKO/bGEEcnnBQ/agQeHubKMoY3Wd5NOzXsBs/NAfFBpFunwNF+v6zgfnEjDQ9XU+zmbrQ=="], + + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.7", "", { "os": "win32", "cpu": "x64" }, "sha512-M9Xw6F3jmXF1+V7DHOPgJRypwp9Gb2ToZHg27/mN7sWkBDEDu9x2m4vdaQIoIEY26PqtcYjPOB5Tm0ESpEzZwQ=="], + "@opentui/react": ["@opentui/react@workspace:packages/react"], "@opentui/solid": ["@opentui/solid@workspace:packages/solid"], @@ -423,8 +436,14 @@ "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], + "tinyest": ["tinyest@0.1.1", "", {}, "sha512-YNHlB8BOXgW6RPzrfqqAkgyY9xj33sjXJcJlOl3MwY0BXXx26m3JUqf5yV8iBdwJPNe51DmxypR9Zbbd266biQ=="], + "token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="], + "typed-binary": ["typed-binary@4.3.2", "", {}, "sha512-HT3pIBM2njCZUmeczDaQUUErGiM6GXFCqMsHegE12HCoBtvHCkfR10JJni0TeGOTnLilTd6YFyj+YhflqQDrDQ=="], + + "typegpu": ["typegpu@0.7.1", "", { "dependencies": { "tinyest": "~0.1.1", "typed-binary": "^4.3.1" } }, "sha512-PyP/ZRsX8lCX27fNzBE5TjwxC8Vj88RcLlCv4VXg5sVd6gBuiPZTJQoC5HR15hjTJoRAfayT/TXQ4phWOkA/aA=="], + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], diff --git a/packages/core/package.json b/packages/core/package.json index 599795e51..de8471da8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -37,7 +37,8 @@ }, "dependencies": { "jimp": "1.6.0", - "yoga-layout": "3.2.1" + "yoga-layout": "3.2.1", + "typegpu": "^0.7.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", diff --git a/packages/core/src/3d/canvas.ts b/packages/core/src/3d/canvas.ts index 72ebc2b1e..c305793b9 100644 --- a/packages/core/src/3d/canvas.ts +++ b/packages/core/src/3d/canvas.ts @@ -1,15 +1,14 @@ import { GPUCanvasContextMock } from "bun-webgpu" +import { tgpu, type TgpuBuffer, type TgpuComputePipeline, type TgpuRoot, type UniformFlag } from "typegpu" import { RGBA } from "../types" import { SuperSampleType } from "./WGPURenderer" import type { OptimizedBuffer } from "../buffer" +import { createSuperSamplingComputeShader, layout as superSamplingLayout, SuperSamplingParams } from './shaders/supersampling' import { toArrayBuffer } from "bun:ffi" import { Jimp } from "jimp" -// @ts-ignore -import shaderTemplate from "./shaders/supersampling.wgsl" with { type: "text" } - const WORKGROUP_SIZE = 4 -const SUPERSAMPLING_COMPUTE_SHADER = shaderTemplate.replace(/\${WORKGROUP_SIZE}/g, WORKGROUP_SIZE.toString()) +const SUPERSAMPLING_COMPUTE_SHADER = await createSuperSamplingComputeShader(WORKGROUP_SIZE) export enum SuperSampleAlgorithm { STANDARD = 0, @@ -18,6 +17,7 @@ export enum SuperSampleAlgorithm { export class CLICanvas { private device: GPUDevice + private root: TgpuRoot private readbackBuffer: GPUBuffer | null = null private width: number private height: number @@ -28,10 +28,9 @@ export class CLICanvas { public superSample: SuperSampleType = SuperSampleType.GPU // Compute shader super sampling - private computePipeline: GPUComputePipeline | null = null - private computeBindGroupLayout: GPUBindGroupLayout | null = null + private computePipeline: TgpuComputePipeline | null = null private computeOutputBuffer: GPUBuffer | null = null - private computeParamsBuffer: GPUBuffer | null = null + private computeParamsBuffer: TgpuBuffer & UniformFlag | null = null private computeReadbackBuffer: GPUBuffer | null = null private updateScheduled: boolean = false private screenshotGPUBuffer: GPUBuffer | null = null @@ -45,6 +44,7 @@ export class CLICanvas { sampleAlgo: SuperSampleAlgorithm = SuperSampleAlgorithm.STANDARD, ) { this.device = device + this.root = tgpu.initFromDevice({ device }) this.width = width this.height = height this.superSample = superSample @@ -163,52 +163,14 @@ export class CLICanvas { private async initComputePipeline(): Promise { if (this.computePipeline) return - const shaderModule = this.device.createShaderModule({ - label: "SuperSampling Compute Shader", - code: SUPERSAMPLING_COMPUTE_SHADER, - }) - - this.computeBindGroupLayout = this.device.createBindGroupLayout({ - label: "SuperSampling Bind Group Layout", - entries: [ - { - binding: 0, - visibility: GPUShaderStage.COMPUTE, - texture: { sampleType: "float", viewDimension: "2d" }, - }, - { - binding: 1, - visibility: GPUShaderStage.COMPUTE, - buffer: { type: "storage" }, - }, - { - binding: 2, - visibility: GPUShaderStage.COMPUTE, - buffer: { type: "uniform" }, - }, - ], - }) + this.computePipeline = this.root['~unstable'] + .withCompute(SUPERSAMPLING_COMPUTE_SHADER) + .createPipeline() - const pipelineLayout = this.device.createPipelineLayout({ - label: "SuperSampling Pipeline Layout", - bindGroupLayouts: [this.computeBindGroupLayout], - }) - - this.computePipeline = this.device.createComputePipeline({ - label: "SuperSampling Compute Pipeline", - layout: pipelineLayout, - compute: { - module: shaderModule, - entryPoint: "main", - }, - }) - - // Create uniform buffer for parameters (8 bytes - 2 u32s: width, height) - this.computeParamsBuffer = this.device.createBuffer({ - label: "SuperSampling Params Buffer", - size: 16, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }) + // Create uniform buffer for parameters + this.computeParamsBuffer = this.root.createBuffer(SuperSamplingParams) + .$usage('uniform') + .$name("SuperSampling Params Buffer") this.updateComputeParams() } @@ -218,14 +180,11 @@ export class CLICanvas { // Update uniform buffer with parameters // Note: this.width/height are render dimensions (2x terminal size for super sampling) - const paramsData = new ArrayBuffer(16) - const uint32View = new Uint32Array(paramsData) - - uint32View[0] = this.width - uint32View[1] = this.height - uint32View[2] = this.superSampleAlgorithm - - this.device.queue.writeBuffer(this.computeParamsBuffer, 0, paramsData) + this.computeParamsBuffer.write({ + width: this.width, + height: this.height, + sampleAlgo: this.superSampleAlgorithm, + }) } private scheduleUpdateComputeBuffers(): void { @@ -278,7 +237,6 @@ export class CLICanvas { if ( !this.computePipeline || - !this.computeBindGroupLayout || !this.computeOutputBuffer || !this.computeParamsBuffer ) { @@ -290,30 +248,24 @@ export class CLICanvas { label: "SuperSampling Input Texture View", }) - const bindGroup = this.device.createBindGroup({ - label: "SuperSampling Bind Group", - layout: this.computeBindGroupLayout, - entries: [ - { binding: 0, resource: textureView }, - { binding: 1, resource: { buffer: this.computeOutputBuffer } }, - { binding: 2, resource: { buffer: this.computeParamsBuffer } }, - ], + const bindGroup = this.root.createBindGroup(superSamplingLayout, { + inputTexture: textureView, + output: this.computeOutputBuffer, + params: this.computeParamsBuffer, }) - const commandEncoder = this.device.createCommandEncoder({ label: "SuperSampling Command Encoder" }) - const computePass = commandEncoder.beginComputePass({ label: "SuperSampling Compute Pass" }) - computePass.setPipeline(this.computePipeline) - computePass.setBindGroup(0, bindGroup) - // Must match WGSL calculation exactly: (params.width + 1u) / 2u const terminalWidthCells = Math.floor((this.width + 1) / 2) const terminalHeightCells = Math.floor((this.height + 1) / 2) const dispatchX = Math.ceil(terminalWidthCells / WORKGROUP_SIZE) const dispatchY = Math.ceil(terminalHeightCells / WORKGROUP_SIZE) - computePass.dispatchWorkgroups(dispatchX, dispatchY, 1) - computePass.end() + this.computePipeline + .with(superSamplingLayout, bindGroup) + .dispatchWorkgroups(dispatchX, dispatchY) + this.root["~unstable"].flush() + const commandEncoder = this.device.createCommandEncoder({ label: "SuperSampling Command Encoder" }) commandEncoder.copyBufferToBuffer( this.computeOutputBuffer, 0, diff --git a/packages/core/src/3d/shaders/supersampling.ts b/packages/core/src/3d/shaders/supersampling.ts new file mode 100644 index 000000000..af639770c --- /dev/null +++ b/packages/core/src/3d/shaders/supersampling.ts @@ -0,0 +1,218 @@ +import tgpu from "typegpu" +import { arrayOf, builtin, f32, size, struct, u32, vec4f } from "typegpu/data" + +export const SuperSamplingParams = struct({ + /** Canvas width in pixels */ + width: u32, + /** Canvas height in pixels */ + height: u32, + /** 0 = standard 2x2, 1 = pre-squeezed horizontal blend */ + sampleAlgo: size(8, u32), + // ^ Padding for 16-byte alignment +}) + +const CellResult = struct({ + /** Background RGBA (16 bytes) */ + bg: vec4f, + /** Foreground RGBA (16 bytes) */ + fg: vec4f, + /** Unicode character code (4 bytes) */ + char: size(16, u32), + // ^ Padding so that the total size is 48 +}) + +const CellBuffer = (n: number) => + struct({ + cells: arrayOf(CellResult, n), + }) + +export const layout = tgpu.bindGroupLayout({ + inputTexture: { texture: 'float', viewDimension: '2d' }, + output: { storage: CellBuffer, access: 'mutable' }, + params: { uniform: SuperSamplingParams }, +}).$idx(0) + +const colorDistance = tgpu.fn([vec4f, vec4f], f32)`(a, b) -> { + let diff = a.rgb - b.rgb; + return dot(diff, diff); +}` + +const luminance = tgpu.fn([vec4f], f32)`(color) { + return 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b; +}` + +const closestColorIndex = tgpu.fn([vec4f, vec4f, vec4f], u32)`(pixel, candA, candB) { + return select(1u, 0u, colorDistance(pixel, candA) <= colorDistance(pixel, candB)); +} +`.$uses({ colorDistance }) + +// NOTE: This is unused, should it be removed? +const averageColor = tgpu.fn([arrayOf(vec4f, 4)], vec4f)`(pixels) { + return (pixels[0] + pixels[1] + pixels[2] + pixels[3]) * 0.25; +}` + +const getPixelColor = tgpu.fn([u32, u32], vec4f)`(pixelX, pixelY) { + if (pixelX >= layout.$.params.width || pixelY >= layout.$.params.height) { + return vec4(0.0, 0.0, 0.0, 1.0); // Black for out-of-bounds + } + + // textureLoad automatically handles format conversion to RGBA + return textureLoad(layout.$.inputTexture, vec2(i32(pixelX), i32(pixelY)), 0); +} +`.$uses({ layout }) + +const blendColors = tgpu.fn([vec4f, vec4f], vec4f)`(color1, color2) { + let a1 = color1.a; + let a2 = color2.a; + + if (a1 == 0.0 && a2 == 0.0) { + return vec4(0.0, 0.0, 0.0, 0.0); + } + + let outAlpha = a1 + a2 - a1 * a2; + if (outAlpha == 0.0) { + return vec4(0.0, 0.0, 0.0, 0.0); + } + + let rgb = (color1.rgb * a1 + color2.rgb * a2 * (1.0 - a1)) / outAlpha; + + return vec4(rgb, outAlpha); +}` + +const averageColorsWithAlpha = tgpu.fn([arrayOf(vec4f, 4)], vec4f)`(pixels) { + let blend1 = blendColors(pixels[0], pixels[1]); + let blend2 = blendColors(pixels[2], pixels[3]); + + return blendColors(blend1, blend2); +} +`.$uses({ blendColors }) + +// Quadrant character lookup table (same as Zig implementation) +const quadrantChars = tgpu["~unstable"].const(arrayOf(u32, 16), [ + 32, // ' ' - 0000 + 0x2597, // ▗ - 0001 BR + 0x2596, // ▖ - 0010 BL + 0x2584, // ▄ - 0011 Lower Half Block + 0x259D, // ▝ - 0100 TR + 0x2590, // ▐ - 0101 Right Half Block + 0x259E, // ▞ - 0110 TR+BL + 0x259F, // ▟ - 0111 TR+BL+BR + 0x2598, // ▘ - 1000 TL + 0x259A, // ▚ - 1001 TL+BR + 0x258C, // ▌ - 1010 Left Half Block + 0x2599, // ▙ - 1011 TL+BL+BR + 0x2580, // ▀ - 1100 Upper Half Block + 0x259C, // ▜ - 1101 TL+TR+BR + 0x259B, // ▛ - 1110 TL+TR+BL + 0x2588 // █ - 1111 Full Block +]) + +const renderQuadrantBlock = tgpu.fn([arrayOf(vec4f, 4)], CellResult)`(pixels) { + var maxDist: f32 = colorDistance(pixels[0], pixels[1]); + var pIdxA: u32 = 0u; + var pIdxB: u32 = 1u; + + for (var i: u32 = 0u; i < 4u; i++) { + for (var j: u32 = i + 1u; j < 4u; j++) { + let dist = colorDistance(pixels[i], pixels[j]); + if (dist > maxDist) { + pIdxA = i; + pIdxB = j; + maxDist = dist; + } + } + } + + let pCandA = pixels[pIdxA]; + let pCandB = pixels[pIdxB]; + + var chosenDarkColor: vec4; + var chosenLightColor: vec4; + + if (luminance(pCandA) <= luminance(pCandB)) { + chosenDarkColor = pCandA; + chosenLightColor = pCandB; + } else { + chosenDarkColor = pCandB; + chosenLightColor = pCandA; + } + + var quadrantBits: u32 = 0u; + let bitValues = array(8u, 4u, 2u, 1u); // TL, TR, BL, BR + + for (var i: u32 = 0u; i < 4u; i++) { + if (closestColorIndex(pixels[i], chosenDarkColor, chosenLightColor) == 0u) { + quadrantBits |= bitValues[i]; + } + } + + // Construct result + var result: CellResult; + + if (quadrantBits == 0u) { // All light + result.char = 32u; // Space character + result.fg = chosenDarkColor; + result.bg = averageColorsWithAlpha(pixels); + } else if (quadrantBits == 15u) { // All dark + result.char = quadrantChars[15]; // Full block + result.fg = averageColorsWithAlpha(pixels); + result.bg = chosenLightColor; + } else { // Mixed pattern + result.char = quadrantChars[quadrantBits]; + result.fg = chosenDarkColor; + result.bg = chosenLightColor; + } + + return result; +} +`.$uses({ CellResult, colorDistance, luminance, closestColorIndex, averageColorsWithAlpha, quadrantChars }) + +export const createSuperSamplingComputeShader = (WORKGROUP_SIZE: number) => { + const main = tgpu["~unstable"].computeFn({ + workgroupSize: [WORKGROUP_SIZE, WORKGROUP_SIZE], + in: { id: builtin.globalInvocationId }, + })`{ + let cellX = in.id.x; + let cellY = in.id.y; + let bufferWidthCells = (layout.$.params.width + 1u) / 2u; + let bufferHeightCells = (layout.$.params.height + 1u) / 2u; + + if (cellX >= bufferWidthCells || cellY >= bufferHeightCells) { + return; + } + + let renderX = cellX * 2u; + let renderY = cellY * 2u; + + var pixelsRgba: array, 4>; + + if (layout.$.params.sampleAlgo == 1u) { + let topColor = getPixelColor(renderX, renderY); + let topColor2 = getPixelColor(renderX + 1u, renderY); + + let blendedTop = blendColors(topColor, topColor2); + + let bottomColor = getPixelColor(renderX, renderY + 1u); + let bottomColor2 = getPixelColor(renderX + 1u, renderY + 1u); + let blendedBottom = blendColors(bottomColor, bottomColor2); + + pixelsRgba[0] = blendedTop; // TL + pixelsRgba[1] = blendedTop; // TR + pixelsRgba[2] = blendedBottom; // BL + pixelsRgba[3] = blendedBottom; // BR + } else { + pixelsRgba[0] = getPixelColor(renderX, renderY); // TL + pixelsRgba[1] = getPixelColor(renderX + 1u, renderY); // TR + pixelsRgba[2] = getPixelColor(renderX, renderY + 1u); // BL + pixelsRgba[3] = getPixelColor(renderX + 1u, renderY + 1u); // BR + } + + let cellResult = renderQuadrantBlock(pixelsRgba); + + let outputIndex = cellY * bufferWidthCells + cellX; + layout.$.output.cells[outputIndex] = cellResult; + } + `.$uses({ layout, renderQuadrantBlock, getPixelColor, blendColors }) + + return main +} \ No newline at end of file diff --git a/packages/core/src/3d/shaders/supersampling.wgsl b/packages/core/src/3d/shaders/supersampling.wgsl deleted file mode 100644 index 514428653..000000000 --- a/packages/core/src/3d/shaders/supersampling.wgsl +++ /dev/null @@ -1,201 +0,0 @@ -struct CellResult { - bg: vec4, // Background RGBA (16 bytes) - fg: vec4, // Foreground RGBA (16 bytes) - char: u32, // Unicode character code (4 bytes) - _padding1: u32, // Padding (4 bytes) - _padding2: u32, // Extra padding (4 bytes) - _padding3: u32, // Extra padding (4 bytes) - total now 48 bytes (16-byte aligned) -}; - -struct CellBuffer { - cells: array -}; - -struct SuperSamplingParams { - width: u32, // Canvas width in pixels - height: u32, // Canvas height in pixels - sampleAlgo: u32, // 0 = standard 2x2, 1 = pre-squeezed horizontal blend - _padding: u32, // Padding for 16-byte alignment -}; - -@group(0) @binding(0) var inputTexture: texture_2d; -@group(0) @binding(1) var output: CellBuffer; -@group(0) @binding(2) var params: SuperSamplingParams; - -// Quadrant character lookup table (same as Zig implementation) -const quadrantChars = array( - 32u, // ' ' - 0000 - 0x2597u, // ▗ - 0001 BR - 0x2596u, // ▖ - 0010 BL - 0x2584u, // ▄ - 0011 Lower Half Block - 0x259Du, // ▝ - 0100 TR - 0x2590u, // ▐ - 0101 Right Half Block - 0x259Eu, // ▞ - 0110 TR+BL - 0x259Fu, // ▟ - 0111 TR+BL+BR - 0x2598u, // ▘ - 1000 TL - 0x259Au, // ▚ - 1001 TL+BR - 0x258Cu, // ▌ - 1010 Left Half Block - 0x2599u, // ▙ - 1011 TL+BL+BR - 0x2580u, // ▀ - 1100 Upper Half Block - 0x259Cu, // ▜ - 1101 TL+TR+BR - 0x259Bu, // ▛ - 1110 TL+TR+BL - 0x2588u // █ - 1111 Full Block -); - -const inv_255: f32 = 1.0 / 255.0; - -fn getPixelColor(pixelX: u32, pixelY: u32) -> vec4 { - if (pixelX >= params.width || pixelY >= params.height) { - return vec4(0.0, 0.0, 0.0, 1.0); // Black for out-of-bounds - } - - // textureLoad automatically handles format conversion to RGBA - return textureLoad(inputTexture, vec2(i32(pixelX), i32(pixelY)), 0); -} - -fn colorDistance(a: vec4, b: vec4) -> f32 { - let diff = a.rgb - b.rgb; - return dot(diff, diff); -} - -fn luminance(color: vec4) -> f32 { - return 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b; -} - -fn closestColorIndex(pixel: vec4, candA: vec4, candB: vec4) -> u32 { - return select(1u, 0u, colorDistance(pixel, candA) <= colorDistance(pixel, candB)); -} - -fn averageColor(pixels: array, 4>) -> vec4 { - return (pixels[0] + pixels[1] + pixels[2] + pixels[3]) * 0.25; -} - -fn blendColors(color1: vec4, color2: vec4) -> vec4 { - let a1 = color1.a; - let a2 = color2.a; - - if (a1 == 0.0 && a2 == 0.0) { - return vec4(0.0, 0.0, 0.0, 0.0); - } - - let outAlpha = a1 + a2 - a1 * a2; - if (outAlpha == 0.0) { - return vec4(0.0, 0.0, 0.0, 0.0); - } - - let rgb = (color1.rgb * a1 + color2.rgb * a2 * (1.0 - a1)) / outAlpha; - - return vec4(rgb, outAlpha); -} - -fn averageColorsWithAlpha(pixels: array, 4>) -> vec4 { - let blend1 = blendColors(pixels[0], pixels[1]); - let blend2 = blendColors(pixels[2], pixels[3]); - - return blendColors(blend1, blend2); -} - -fn renderQuadrantBlock(pixels: array, 4>) -> CellResult { - var maxDist: f32 = colorDistance(pixels[0], pixels[1]); - var pIdxA: u32 = 0u; - var pIdxB: u32 = 1u; - - for (var i: u32 = 0u; i < 4u; i++) { - for (var j: u32 = i + 1u; j < 4u; j++) { - let dist = colorDistance(pixels[i], pixels[j]); - if (dist > maxDist) { - pIdxA = i; - pIdxB = j; - maxDist = dist; - } - } - } - - let pCandA = pixels[pIdxA]; - let pCandB = pixels[pIdxB]; - - var chosenDarkColor: vec4; - var chosenLightColor: vec4; - - if (luminance(pCandA) <= luminance(pCandB)) { - chosenDarkColor = pCandA; - chosenLightColor = pCandB; - } else { - chosenDarkColor = pCandB; - chosenLightColor = pCandA; - } - - var quadrantBits: u32 = 0u; - let bitValues = array(8u, 4u, 2u, 1u); // TL, TR, BL, BR - - for (var i: u32 = 0u; i < 4u; i++) { - if (closestColorIndex(pixels[i], chosenDarkColor, chosenLightColor) == 0u) { - quadrantBits |= bitValues[i]; - } - } - - // Construct result - var result: CellResult; - - if (quadrantBits == 0u) { // All light - result.char = 32u; // Space character - result.fg = chosenDarkColor; - result.bg = averageColorsWithAlpha(pixels); - } else if (quadrantBits == 15u) { // All dark - result.char = quadrantChars[15]; // Full block - result.fg = averageColorsWithAlpha(pixels); - result.bg = chosenLightColor; - } else { // Mixed pattern - result.char = quadrantChars[quadrantBits]; - result.fg = chosenDarkColor; - result.bg = chosenLightColor; - } - result._padding1 = 0u; - result._padding2 = 0u; - result._padding3 = 0u; - - return result; -} - -@compute @workgroup_size(${WORKGROUP_SIZE}, ${WORKGROUP_SIZE}, 1) -fn main(@builtin(global_invocation_id) id: vec3) { - let cellX = id.x; - let cellY = id.y; - let bufferWidthCells = (params.width + 1u) / 2u; - let bufferHeightCells = (params.height + 1u) / 2u; - - if (cellX >= bufferWidthCells || cellY >= bufferHeightCells) { - return; - } - - let renderX = cellX * 2u; - let renderY = cellY * 2u; - - var pixelsRgba: array, 4>; - - if (params.sampleAlgo == 1u) { - let topColor = getPixelColor(renderX, renderY); - let topColor2 = getPixelColor(renderX + 1u, renderY); - - let blendedTop = blendColors(topColor, topColor2); - - let bottomColor = getPixelColor(renderX, renderY + 1u); - let bottomColor2 = getPixelColor(renderX + 1u, renderY + 1u); - let blendedBottom = blendColors(bottomColor, bottomColor2); - - pixelsRgba[0] = blendedTop; // TL - pixelsRgba[1] = blendedTop; // TR - pixelsRgba[2] = blendedBottom; // BL - pixelsRgba[3] = blendedBottom; // BR - } else { - pixelsRgba[0] = getPixelColor(renderX, renderY); // TL - pixelsRgba[1] = getPixelColor(renderX + 1u, renderY); // TR - pixelsRgba[2] = getPixelColor(renderX, renderY + 1u); // BL - pixelsRgba[3] = getPixelColor(renderX + 1u, renderY + 1u); // BR - } - - let cellResult = renderQuadrantBlock(pixelsRgba); - - let outputIndex = cellY * bufferWidthCells + cellX; - output.cells[outputIndex] = cellResult; -} \ No newline at end of file