From c98382b6fefdf8a2c6b126171ed549720df09d16 Mon Sep 17 00:00:00 2001 From: Jaremy Creechley Date: Thu, 11 Jun 2026 21:36:12 -0600 Subject: [PATCH] add fixed fast rect mask limit --- README.md | 2 +- src/figdraw/figbackend.nim | 5 + src/figdraw/metal/metal_context.nim | 232 ++++++++++-- src/figdraw/metal/metal_shaders.metal | 33 +- src/figdraw/opengl/glcontext.nim | 227 +++++++----- src/figdraw/opengl/glsl/atlas.frag | 5 +- src/figdraw/opengl/glsl/atlas.vert | 7 +- src/figdraw/opengl/glsl/atlas_rect_mask.frag | 37 +- src/figdraw/opengl/glsl/atlas_rect_mask.vert | 22 +- src/figdraw/opengl/glsl/emscripten/atlas.frag | 5 +- src/figdraw/opengl/glsl/emscripten/atlas.vert | 7 +- .../glsl/emscripten/atlas_rect_mask.frag | 37 +- .../glsl/emscripten/atlas_rect_mask.vert | 22 +- src/figdraw/opengl/glsl/emscripten/mask.frag | 1 - src/figdraw/opengl/glsl/mask.frag | 1 - src/figdraw/opengl/shaders.nim | 30 +- src/figdraw/vulkan/shaders/sdf.frag.glsl | 34 +- src/figdraw/vulkan/shaders/sdf.frag.spv | Bin 18780 -> 19996 bytes .../vulkan/shaders/sdf.rectmask0.frag.spv | Bin 0 -> 19368 bytes .../vulkan/shaders/sdf.rectmask0.vert.spv | Bin 0 -> 3200 bytes .../vulkan/shaders/sdf.rectmask1.frag.spv | Bin 0 -> 19368 bytes .../vulkan/shaders/sdf.rectmask1.vert.spv | Bin 0 -> 3200 bytes src/figdraw/vulkan/shaders/sdf.vert.glsl | 18 + src/figdraw/vulkan/shaders/sdf.vert.spv | Bin 3272 -> 3820 bytes src/figdraw/vulkan/vulkan_context.nim | 348 ++++++++++++------ 25 files changed, 791 insertions(+), 282 deletions(-) create mode 100644 src/figdraw/vulkan/shaders/sdf.rectmask0.frag.spv create mode 100644 src/figdraw/vulkan/shaders/sdf.rectmask0.vert.spv create mode 100644 src/figdraw/vulkan/shaders/sdf.rectmask1.frag.spv create mode 100644 src/figdraw/vulkan/shaders/sdf.rectmask1.vert.spv diff --git a/README.md b/README.md index e7cacf4b..8a87f29c 100644 --- a/README.md +++ b/README.md @@ -362,7 +362,7 @@ Notes: Use `NfClipContent` when a node needs normal clipping semantics. It renders a mask and applies it to the node's content, which is flexible but can force the backend to flush queued draws around the mask pass. -Use `NfRectMaskContent` when the mask shape is just the node's rounded rectangle and the content is small leaf-style UI content, such as cells in a list/table, clipped buttons, pills, badges, or compact panels. On Metal this is evaluated as a per-fragment rounded-rect SDF mask, so it can avoid the extra mask pass for the first active rect mask and keep more draw work batched. Other backends fall back to the normal mask behavior, so the flag is safe to use before every backend has a fast implementation. +Use `NfRectMaskContent` when the mask shape is just the node's rounded rectangle and the content is small leaf-style UI content, such as cells in a list/table, clipped buttons, pills, badges, or compact panels. Metal, OpenGL, and Vulkan evaluate the first active rect masks as per-fragment rounded-rect SDF masks, so they can avoid extra mask passes and keep more draw work batched. The number of fast rect masks is fixed at compile time with `-d:FigDrawFastRectMaskLimit=0`, `1`, or `2`; the default is `2`. Deeper rect masks fall back to normal mask behavior. `NfRectMaskContent` also composes with `NfClipContent`: a scroll viewport can use `NfClipContent`, while each small child item inside it uses `NfRectMaskContent`. diff --git a/src/figdraw/figbackend.nim b/src/figdraw/figbackend.nim index 8707adb6..75a6ddb0 100644 --- a/src/figdraw/figbackend.nim +++ b/src/figdraw/figbackend.nim @@ -44,6 +44,11 @@ type SdfMode* {.pure.} = enum sdfModeMtsdfAnnular = 16 sdfModeBackdropBlur = 17 +const FigDrawFastRectMaskLimit* {.intdefine.}: int = 2 +static: + doAssert FigDrawFastRectMaskLimit in 0 .. 2, + "FigDrawFastRectMaskLimit must be 0, 1, or 2" + type BackendFillKind* = enum bfColor diff --git a/src/figdraw/metal/metal_context.nim b/src/figdraw/metal/metal_context.nim index 4669058f..5569832f 100644 --- a/src/figdraw/metal/metal_context.nim +++ b/src/figdraw/metal/metal_context.nim @@ -27,6 +27,13 @@ proc round*(v: Vec2): Vec2 = const quadLimit = 10_921 const maxFramesInFlight = 3 +const fastRectMaskLimit = figbackend.FigDrawFastRectMaskLimit +when fastRectMaskLimit >= 2: + const rectMaskUniformBufferIndex = 17 +else: + const rectMaskUniformBufferIndex = 13 +const metalShaderDefines = + "#define FIGDRAW_FAST_RECT_MASK_LIMIT " & $fastRectMaskLimit & "\n" type PassKind = enum pkNone @@ -74,6 +81,15 @@ type FlushBuffers = object rectMaskMatXCapacity: int rectMaskMatY: ObjcOwned[MTLBuffer] rectMaskMatYCapacity: int + when fastRectMaskLimit >= 2: + rectMaskParams2: ObjcOwned[MTLBuffer] + rectMaskParams2Capacity: int + rectMaskRadii2: ObjcOwned[MTLBuffer] + rectMaskRadii2Capacity: int + rectMaskMatX2: ObjcOwned[MTLBuffer] + rectMaskMatX2Capacity: int + rectMaskMatY2: ObjcOwned[MTLBuffer] + rectMaskMatY2Capacity: int type FrameArena = object flushBuffers: seq[FlushBuffers] @@ -139,6 +155,11 @@ type MetalContext* = ref object of figbackend.BackendContext # Metal objects rectMaskRadii: tuple[buffer: ObjcOwned[MTLBuffer], data: seq[float32]] rectMaskMatX: tuple[buffer: ObjcOwned[MTLBuffer], data: seq[float32]] rectMaskMatY: tuple[buffer: ObjcOwned[MTLBuffer], data: seq[float32]] + when fastRectMaskLimit >= 2: + rectMaskParams2: tuple[buffer: ObjcOwned[MTLBuffer], data: seq[float32]] + rectMaskRadii2: tuple[buffer: ObjcOwned[MTLBuffer], data: seq[float32]] + rectMaskMatX2: tuple[buffer: ObjcOwned[MTLBuffer], data: seq[float32]] + rectMaskMatY2: tuple[buffer: ObjcOwned[MTLBuffer], data: seq[float32]] rectMaskStack: seq[RectMask] # SDF shader uniform (global) @@ -407,7 +428,7 @@ proc ensureDeviceAndPipelines(ctx: MetalContext) = raise newException(ValueError, "Failed to create Metal command queue") ctx.queue.resetRetained(q) - let shaderSource = metalShaderSource + let shaderSource = metalShaderDefines & metalShaderSource var err: NSError let library = fromRetained( @@ -597,6 +618,27 @@ proc upload(ctx: MetalContext) = ctx.rectMaskMatY.data, vertexCount * 4 * sizeof(float32), ) + when fastRectMaskLimit >= 2: + copyToBuf( + ctx.rectMaskParams2.buffer.borrow, + ctx.rectMaskParams2.data, + vertexCount * 4 * sizeof(float32), + ) + copyToBuf( + ctx.rectMaskRadii2.buffer.borrow, + ctx.rectMaskRadii2.data, + vertexCount * 4 * sizeof(float32), + ) + copyToBuf( + ctx.rectMaskMatX2.buffer.borrow, + ctx.rectMaskMatX2.data, + vertexCount * 4 * sizeof(float32), + ) + copyToBuf( + ctx.rectMaskMatY2.buffer.borrow, + ctx.rectMaskMatY2.data, + vertexCount * 4 * sizeof(float32), + ) proc grow(ctx: MetalContext) = ctx.flush() @@ -751,6 +793,16 @@ proc setRectMaskVert4(ctx: MetalContext, offset: int, params, radii, matX, matY: ctx.rectMaskMatX.data.setVert4(offset + i, matX) ctx.rectMaskMatY.data.setVert4(offset + i, matY) +when fastRectMaskLimit >= 2: + proc setRectMask2Vert4( + ctx: MetalContext, offset: int, params, radii, matX, matY: Vec4 + ) = + for i in 0 ..< 4: + ctx.rectMaskParams2.data.setVert4(offset + i, params) + ctx.rectMaskRadii2.data.setVert4(offset + i, radii) + ctx.rectMaskMatX2.data.setVert4(offset + i, matX) + ctx.rectMaskMatY2.data.setVert4(offset + i, matY) + proc setDisabledRectMaskVerts(ctx: MetalContext, firstVertex, vertexCount: int) = let params = vec4(0.0'f32, 0.0'f32, -1.0'f32, -1.0'f32) @@ -760,40 +812,77 @@ proc setDisabledRectMaskVerts(ctx: MetalContext, firstVertex, vertexCount: int) ctx.rectMaskRadii.data.setVert4(i, zero4) ctx.rectMaskMatX.data.setVert4(i, zero4) ctx.rectMaskMatY.data.setVert4(i, zero4) + when fastRectMaskLimit >= 2: + ctx.rectMaskParams2.data.setVert4(i, params) + ctx.rectMaskRadii2.data.setVert4(i, zero4) + ctx.rectMaskMatX2.data.setVert4(i, zero4) + ctx.rectMaskMatY2.data.setVert4(i, zero4) + +proc fastRectMaskCount(ctx: MetalContext): int = + for rectMask in ctx.rectMaskStack: + if rectMask.kind == rmkFast: + inc result proc setRectMaskVert4(ctx: MetalContext, offset: int) = - if ctx.maskBegun: + when fastRectMaskLimit == 0: return + else: + if ctx.maskBegun: + return - var - hasRectMask = false - params = vec4(0.0'f32) - radii = vec4(0.0'f32) - matX = vec4(0.0'f32) - matY = vec4(0.0'f32) - - if ctx.rectMaskStack.len > 0: - for i in countdown(ctx.rectMaskStack.len - 1, 0): - let rectMask = ctx.rectMaskStack[i] - if rectMask.kind == rmkFast: - hasRectMask = true - params = rectMask.params - radii = rectMask.radii - matX = rectMask.matX - matY = rectMask.matY - break - - if hasRectMask: - if not ctx.batchHasRectMask: - ctx.setDisabledRectMaskVerts(0, offset) - ctx.batchHasRectMask = true - ctx.setRectMaskVert4(offset, params, radii, matX, matY) - elif ctx.batchHasRectMask: - ctx.setDisabledRectMaskVerts(offset, 4) + var + maskCount = 0 + params = vec4(0.0'f32) + radii = vec4(0.0'f32) + matX = vec4(0.0'f32) + matY = vec4(0.0'f32) + when fastRectMaskLimit >= 2: + var + params2 = vec4(0.0'f32) + radii2 = vec4(0.0'f32) + matX2 = vec4(0.0'f32) + matY2 = vec4(0.0'f32) + + if ctx.rectMaskStack.len > 0: + for i in countdown(ctx.rectMaskStack.len - 1, 0): + let rectMask = ctx.rectMaskStack[i] + if rectMask.kind == rmkFast: + if maskCount == 0: + params = rectMask.params + radii = rectMask.radii + matX = rectMask.matX + matY = rectMask.matY + inc maskCount + elif fastRectMaskLimit >= 2 and maskCount == 1: + when fastRectMaskLimit >= 2: + params2 = rectMask.params + radii2 = rectMask.radii + matX2 = rectMask.matX + matY2 = rectMask.matY + inc maskCount + if maskCount >= fastRectMaskLimit: + break + + if maskCount > 0: + if not ctx.batchHasRectMask: + ctx.setDisabledRectMaskVerts(0, offset) + ctx.batchHasRectMask = true + ctx.setRectMaskVert4(offset, params, radii, matX, matY) + when fastRectMaskLimit >= 2: + if maskCount >= 2: + ctx.setRectMask2Vert4(offset, params2, radii2, matX2, matY2) + else: + let + disabledParams = vec4(0.0'f32, 0.0'f32, -1.0'f32, -1.0'f32) + zero4 = vec4(0.0'f32) + ctx.setRectMask2Vert4(offset, disabledParams, zero4, zero4, zero4) + elif ctx.batchHasRectMask: + ctx.setDisabledRectMaskVerts(offset, 4) template setRectMaskVert4IfNeeded(ctx: MetalContext, offset: int) = - if not ctx.maskBegun and (ctx.batchHasRectMask or ctx.rectMaskStack.len > 0): - ctx.setRectMaskVert4(offset) + when fastRectMaskLimit > 0: + if not ctx.maskBegun and (ctx.batchHasRectMask or ctx.rectMaskStack.len > 0): + ctx.setRectMaskVert4(offset) func `*`*(m: Mat4, v: Vec2): Vec2 = (m * vec3(v.x, v.y, 0.0)).xy @@ -1554,7 +1643,8 @@ method beginRectMask*( assert ctx.frameBegun == true, "ctx.beginFrame has not been called." assert ctx.maskBegun == false, "ctx.beginRectMask cannot start inside a mask." - if ctx.rectMaskStack.len == 0 and maskRect.w > 0.0'f32 and maskRect.h > 0.0'f32: + if fastRectMaskLimit > 0 and ctx.fastRectMaskCount() < fastRectMaskLimit and + maskRect.w > 0.0'f32 and maskRect.h > 0.0'f32: ctx.rectMaskStack.add(ctx.makeRectMask(maskRect, radii)) else: ctx.beginMask(maskRect, radii) @@ -1925,6 +2015,27 @@ proc flush(ctx: MetalContext, maskTextureRead: int = ctx.maskTextureWrite) = flushBuffers[].rectMaskMatYCapacity, rectMaskMatYBytes, ) + when fastRectMaskLimit >= 2: + ctx.ensureFlushBufferCapacity( + flushBuffers[].rectMaskParams2, + flushBuffers[].rectMaskParams2Capacity, + rectMaskParamsBytes, + ) + ctx.ensureFlushBufferCapacity( + flushBuffers[].rectMaskRadii2, + flushBuffers[].rectMaskRadii2Capacity, + rectMaskRadiiBytes, + ) + ctx.ensureFlushBufferCapacity( + flushBuffers[].rectMaskMatX2, + flushBuffers[].rectMaskMatX2Capacity, + rectMaskMatXBytes, + ) + ctx.ensureFlushBufferCapacity( + flushBuffers[].rectMaskMatY2, + flushBuffers[].rectMaskMatY2Capacity, + rectMaskMatYBytes, + ) copyToBuf(flushBuffers[].positions.borrow, ctx.positions.data, positionsBytes) copyToBuf(flushBuffers[].uvs.borrow, ctx.uvs.data, uvsBytes) @@ -1957,6 +2068,23 @@ proc flush(ctx: MetalContext, maskTextureRead: int = ctx.maskTextureWrite) = copyToBuf( flushBuffers[].rectMaskMatY.borrow, ctx.rectMaskMatY.data, rectMaskMatYBytes ) + when fastRectMaskLimit >= 2: + copyToBuf( + flushBuffers[].rectMaskParams2.borrow, + ctx.rectMaskParams2.data, + rectMaskParamsBytes, + ) + copyToBuf( + flushBuffers[].rectMaskRadii2.borrow, + ctx.rectMaskRadii2.data, + rectMaskRadiiBytes, + ) + copyToBuf( + flushBuffers[].rectMaskMatX2.borrow, ctx.rectMaskMatX2.data, rectMaskMatXBytes + ) + copyToBuf( + flushBuffers[].rectMaskMatY2.borrow, ctx.rectMaskMatY2.data, rectMaskMatYBytes + ) setVertexBuffer(enc, flushBuffers[].positions.borrow, 0, 0) setVertexBuffer(enc, flushBuffers[].uvs.borrow, 0, 1) @@ -1972,13 +2100,21 @@ proc flush(ctx: MetalContext, maskTextureRead: int = ctx.maskTextureWrite) = setVertexBuffer(enc, flushBuffers[].rectMaskRadii.borrow, 0, 10) setVertexBuffer(enc, flushBuffers[].rectMaskMatX.borrow, 0, 11) setVertexBuffer(enc, flushBuffers[].rectMaskMatY.borrow, 0, 12) + when fastRectMaskLimit >= 2: + setVertexBuffer(enc, flushBuffers[].rectMaskParams2.borrow, 0, 13) + setVertexBuffer(enc, flushBuffers[].rectMaskRadii2.borrow, 0, 14) + setVertexBuffer(enc, flushBuffers[].rectMaskMatX2.borrow, 0, 15) + setVertexBuffer(enc, flushBuffers[].rectMaskMatY2.borrow, 0, 16) type VSUniforms = object proj: Mat4 var vsu = VSUniforms(proj: ctx.proj) setVertexBytes( - enc, addr vsu, NSUInteger(sizeof(VSUniforms)), (if useRectMaskPipeline: 13 else: 9) + enc, + addr vsu, + NSUInteger(sizeof(VSUniforms)), + (if useRectMaskPipeline: rectMaskUniformBufferIndex else: 9), ) type FSUniforms = object @@ -2061,6 +2197,11 @@ proc newContext*( result.rectMaskRadii.data = newSeq[float32](4 * maxQuads * 4) result.rectMaskMatX.data = newSeq[float32](4 * maxQuads * 4) result.rectMaskMatY.data = newSeq[float32](4 * maxQuads * 4) + when fastRectMaskLimit >= 2: + result.rectMaskParams2.data = newSeq[float32](4 * maxQuads * 4) + result.rectMaskRadii2.data = newSeq[float32](4 * maxQuads * 4) + result.rectMaskMatX2.data = newSeq[float32](4 * maxQuads * 4) + result.rectMaskMatY2.data = newSeq[float32](4 * maxQuads * 4) result.rectMaskStack = @[] # Allocate GPU buffers. @@ -2155,6 +2296,35 @@ proc newContext*( MTLResourceOptions(0), ) ) + when fastRectMaskLimit >= 2: + result.rectMaskParams2.buffer.resetRetained( + newBufferWithLength( + result.device.borrow, + NSUInteger(result.rectMaskParams2.data.len * sizeof(float32)), + MTLResourceOptions(0), + ) + ) + result.rectMaskRadii2.buffer.resetRetained( + newBufferWithLength( + result.device.borrow, + NSUInteger(result.rectMaskRadii2.data.len * sizeof(float32)), + MTLResourceOptions(0), + ) + ) + result.rectMaskMatX2.buffer.resetRetained( + newBufferWithLength( + result.device.borrow, + NSUInteger(result.rectMaskMatX2.data.len * sizeof(float32)), + MTLResourceOptions(0), + ) + ) + result.rectMaskMatY2.buffer.resetRetained( + newBufferWithLength( + result.device.borrow, + NSUInteger(result.rectMaskMatY2.data.len * sizeof(float32)), + MTLResourceOptions(0), + ) + ) # Indices are static. result.indices.data = newSeq[uint16](maxQuads * 6) diff --git a/src/figdraw/metal/metal_shaders.metal b/src/figdraw/metal/metal_shaders.metal index f280a229..36ffb0a7 100644 --- a/src/figdraw/metal/metal_shaders.metal +++ b/src/figdraw/metal/metal_shaders.metal @@ -29,6 +29,12 @@ struct RectMaskVSOut { float4 rectMaskRadii; float4 rectMaskMatX; float4 rectMaskMatY; +#if FIGDRAW_FAST_RECT_MASK_LIMIT >= 2 + float4 rectMaskParams2; + float4 rectMaskRadii2; + float4 rectMaskMatX2; + float4 rectMaskMatY2; +#endif }; struct VSUniforms { @@ -177,7 +183,16 @@ vertex RectMaskVSOut vs_rect_mask( const device float4* rectMaskRadii [[buffer(10)]], const device float4* rectMaskMatX [[buffer(11)]], const device float4* rectMaskMatY [[buffer(12)]], - constant VSUniforms& u [[buffer(13)]]) { +#if FIGDRAW_FAST_RECT_MASK_LIMIT >= 2 + const device float4* rectMaskParams2 [[buffer(13)]], + const device float4* rectMaskRadii2 [[buffer(14)]], + const device float4* rectMaskMatX2 [[buffer(15)]], + const device float4* rectMaskMatY2 [[buffer(16)]], + constant VSUniforms& u [[buffer(17)]] +#else + constant VSUniforms& u [[buffer(13)]] +#endif + ) { RectMaskVSOut out; float2 p = positions[vid]; out.position = u.proj * float4(p.x, p.y, 0.0, 1.0); @@ -194,6 +209,12 @@ vertex RectMaskVSOut vs_rect_mask( out.rectMaskRadii = rectMaskRadii[vid]; out.rectMaskMatX = rectMaskMatX[vid]; out.rectMaskMatY = rectMaskMatY[vid]; +#if FIGDRAW_FAST_RECT_MASK_LIMIT >= 2 + out.rectMaskParams2 = rectMaskParams2[vid]; + out.rectMaskRadii2 = rectMaskRadii2[vid]; + out.rectMaskMatX2 = rectMaskMatX2[vid]; + out.rectMaskMatY2 = rectMaskMatY2[vid]; +#endif return out; } @@ -400,6 +421,16 @@ fragment float4 fs_rect_mask( in.rectMaskMatY, u.aaFactor ); +#if FIGDRAW_FAST_RECT_MASK_LIMIT >= 2 + fragColor.w *= rectMaskAlpha( + in.pos, + in.rectMaskParams2, + in.rectMaskRadii2, + in.rectMaskMatX2, + in.rectMaskMatY2, + u.aaFactor + ); +#endif return fragColor; } diff --git a/src/figdraw/opengl/glcontext.nim b/src/figdraw/opengl/glcontext.nim index 295901f6..8fe87cad 100644 --- a/src/figdraw/opengl/glcontext.nim +++ b/src/figdraw/opengl/glcontext.nim @@ -26,6 +26,9 @@ proc round*(v: Vec2): Vec2 = vec2(round(v.x), round(v.y)) const quadLimit = 10_921 +const fastRectMaskLimit = figbackend.FigDrawFastRectMaskLimit +const rectMaskShaderDefines = + "#define FIGDRAW_FAST_RECT_MASK_LIMIT " & $fastRectMaskLimit & "\n" when defined(emscripten): type SdfModeData = float32 @@ -83,12 +86,16 @@ type OpenGlContext* = ref object of figbackend.BackendContext sdfRadii: tuple[buffer: Buffer, data: seq[float32]] ## Vec4: (topRight, bottomRight, topLeft, bottomLeft) sdfModeAttr: tuple[buffer: Buffer, data: seq[SdfModeData]] ## SDFMode value - sdfFactors: tuple[buffer: Buffer, data: seq[float32]] ## Vec2: (factor, spread) - subpixelShifts: tuple[buffer: Buffer, data: seq[float32]] ## Scalar shift in px + sdfFactors: tuple[buffer: Buffer, data: seq[float32]] + ## Vec4: (factor, spread, text subpixel shift, unused) rectMaskParams: tuple[buffer: Buffer, data: seq[float32]] rectMaskRadii: tuple[buffer: Buffer, data: seq[float32]] rectMaskMatX: tuple[buffer: Buffer, data: seq[float32]] rectMaskMatY: tuple[buffer: Buffer, data: seq[float32]] + when fastRectMaskLimit >= 2: + rectMaskParams2: tuple[buffer: Buffer, data: seq[float32]] + rectMaskRadii2: tuple[buffer: Buffer, data: seq[float32]] + rectMaskMat2: tuple[buffer: Buffer, data: seq[float32]] rectMaskStack: seq[RectMask] # SDF shader uniforms (global) @@ -125,12 +132,10 @@ proc upload(ctx: OpenGlContext) = ctx.sdfRadii.buffer.count = ctx.quadCount * 4 ctx.sdfModeAttr.buffer.count = ctx.quadCount * 4 ctx.sdfFactors.buffer.count = ctx.quadCount * 4 - ctx.subpixelShifts.buffer.count = ctx.quadCount * 4 bindBufferData(ctx.sdfParams.buffer.addr, ctx.sdfParams.data[0].addr) bindBufferData(ctx.sdfRadii.buffer.addr, ctx.sdfRadii.data[0].addr) bindBufferData(ctx.sdfModeAttr.buffer.addr, ctx.sdfModeAttr.data[0].addr) bindBufferData(ctx.sdfFactors.buffer.addr, ctx.sdfFactors.data[0].addr) - bindBufferData(ctx.subpixelShifts.buffer.addr, ctx.subpixelShifts.data[0].addr) proc uploadRectMasks(ctx: OpenGlContext) = ctx.rectMaskParams.buffer.count = ctx.quadCount * 4 @@ -141,6 +146,13 @@ proc uploadRectMasks(ctx: OpenGlContext) = bindBufferData(ctx.rectMaskRadii.buffer.addr, ctx.rectMaskRadii.data[0].addr) bindBufferData(ctx.rectMaskMatX.buffer.addr, ctx.rectMaskMatX.data[0].addr) bindBufferData(ctx.rectMaskMatY.buffer.addr, ctx.rectMaskMatY.data[0].addr) + when fastRectMaskLimit >= 2: + ctx.rectMaskParams2.buffer.count = ctx.quadCount * 4 + ctx.rectMaskRadii2.buffer.count = ctx.quadCount * 4 + ctx.rectMaskMat2.buffer.count = ctx.quadCount * 4 + bindBufferData(ctx.rectMaskParams2.buffer.addr, ctx.rectMaskParams2.data[0].addr) + bindBufferData(ctx.rectMaskRadii2.buffer.addr, ctx.rectMaskRadii2.data[0].addr) + bindBufferData(ctx.rectMaskMat2.buffer.addr, ctx.rectMaskMat2.data[0].addr) proc setUpMaskFramebuffer(ctx: OpenGlContext) = glBindFramebuffer(GL_FRAMEBUFFER, ctx.maskFramebufferId) @@ -274,8 +286,9 @@ proc newContext*( newShaderStatic("glsl/emscripten/atlas.vert", "glsl/emscripten/mask.frag") result.mainShader = newShaderStatic("glsl/emscripten/atlas.vert", "glsl/emscripten/atlas.frag") - result.rectMaskShader = newShaderStatic( - "glsl/emscripten/atlas_rect_mask.vert", "glsl/emscripten/atlas_rect_mask.frag" + result.rectMaskShader = newShaderStaticWithDefines( + "glsl/emscripten/atlas_rect_mask.vert", "glsl/emscripten/atlas_rect_mask.frag", + rectMaskShaderDefines, ) result.blurShader = newShaderStatic("glsl/emscripten/blur.vert", "glsl/emscripten/blur.frag") @@ -283,8 +296,9 @@ proc newContext*( try: result.maskShader = newShaderStatic("glsl/atlas.vert", "glsl/mask.frag") result.mainShader = newShaderStatic("glsl/atlas.vert", "glsl/atlas.frag") - result.rectMaskShader = - newShaderStatic("glsl/atlas_rect_mask.vert", "glsl/atlas_rect_mask.frag") + result.rectMaskShader = newShaderStaticWithDefines( + "glsl/atlas_rect_mask.vert", "glsl/atlas_rect_mask.frag", rectMaskShaderDefines + ) result.blurShader = newShaderStatic("glsl/blur.vert", "glsl/blur.frag") except ShaderCompilationError: info "OpenGL 3.30 failed, trying GLSL ES fallback" @@ -292,8 +306,9 @@ proc newContext*( newShaderStatic("glsl/emscripten/atlas.vert", "glsl/emscripten/mask.frag") result.mainShader = newShaderStatic("glsl/emscripten/atlas.vert", "glsl/emscripten/atlas.frag") - result.rectMaskShader = newShaderStatic( - "glsl/emscripten/atlas_rect_mask.vert", "glsl/emscripten/atlas_rect_mask.frag" + result.rectMaskShader = newShaderStaticWithDefines( + "glsl/emscripten/atlas_rect_mask.vert", "glsl/emscripten/atlas_rect_mask.frag", + rectMaskShaderDefines, ) result.blurShader = newShaderStatic("glsl/emscripten/blur.vert", "glsl/emscripten/blur.frag") @@ -361,18 +376,12 @@ proc newContext*( newSeq[SdfModeData](result.sdfModeAttr.buffer.kind.componentCount() * maxQuads * 4) result.sdfFactors.buffer.componentType = cGL_FLOAT - result.sdfFactors.buffer.kind = bkVEC2 + result.sdfFactors.buffer.kind = bkVEC4 result.sdfFactors.buffer.target = GL_ARRAY_BUFFER result.sdfFactors.buffer.usage = GL_STREAM_DRAW result.sdfFactors.data = newSeq[float32](result.sdfFactors.buffer.kind.componentCount() * maxQuads * 4) - result.subpixelShifts.buffer.componentType = cGL_FLOAT - result.subpixelShifts.buffer.kind = bkSCALAR - result.subpixelShifts.buffer.target = GL_ARRAY_BUFFER - result.subpixelShifts.buffer.usage = GL_STREAM_DRAW - result.subpixelShifts.data = newSeq[float32](maxQuads * 4) - result.rectMaskParams.buffer.componentType = cGL_FLOAT result.rectMaskParams.buffer.kind = bkVEC4 result.rectMaskParams.buffer.target = GL_ARRAY_BUFFER @@ -401,6 +410,29 @@ proc newContext*( result.rectMaskMatY.data = newSeq[float32](result.rectMaskMatY.buffer.kind.componentCount() * maxQuads * 4) + when fastRectMaskLimit >= 2: + result.rectMaskParams2.buffer.componentType = cGL_FLOAT + result.rectMaskParams2.buffer.kind = bkVEC4 + result.rectMaskParams2.buffer.target = GL_ARRAY_BUFFER + result.rectMaskParams2.buffer.usage = GL_STREAM_DRAW + result.rectMaskParams2.data = newSeq[float32]( + result.rectMaskParams2.buffer.kind.componentCount() * maxQuads * 4 + ) + + result.rectMaskRadii2.buffer.componentType = cGL_FLOAT + result.rectMaskRadii2.buffer.kind = bkVEC4 + result.rectMaskRadii2.buffer.target = GL_ARRAY_BUFFER + result.rectMaskRadii2.buffer.usage = GL_STREAM_DRAW + result.rectMaskRadii2.data = + newSeq[float32](result.rectMaskRadii2.buffer.kind.componentCount() * maxQuads * 4) + + result.rectMaskMat2.buffer.componentType = cGL_FLOAT + result.rectMaskMat2.buffer.kind = bkVEC4 + result.rectMaskMat2.buffer.target = GL_ARRAY_BUFFER + result.rectMaskMat2.buffer.usage = GL_STREAM_DRAW + result.rectMaskMat2.data = + newSeq[float32](result.rectMaskMat2.buffer.kind.componentCount() * maxQuads * 4) + result.indices.buffer.componentType = GL_UNSIGNED_SHORT result.indices.buffer.kind = bkSCALAR result.indices.buffer.target = GL_ELEMENT_ARRAY_BUFFER @@ -440,7 +472,6 @@ proc newContext*( result.mainShader.bindAttrib("vertexSdfRadii", result.sdfRadii.buffer) result.mainShader.bindAttrib("vertexSdfMode", result.sdfModeAttr.buffer) result.mainShader.bindAttrib("vertexSdfFactors", result.sdfFactors.buffer) - result.mainShader.bindAttrib("vertexSubpixelShift", result.subpixelShifts.buffer) # Main shader with per-vertex fast rect masks. glGenVertexArrays(1, result.rectMaskVertexArrayId.addr) @@ -454,11 +485,18 @@ proc newContext*( result.rectMaskShader.bindAttrib("vertexSdfRadii", result.sdfRadii.buffer) result.rectMaskShader.bindAttrib("vertexSdfMode", result.sdfModeAttr.buffer) result.rectMaskShader.bindAttrib("vertexSdfFactors", result.sdfFactors.buffer) - result.rectMaskShader.bindAttrib("vertexSubpixelShift", result.subpixelShifts.buffer) result.rectMaskShader.bindAttrib("vertexRectMaskParams", result.rectMaskParams.buffer) result.rectMaskShader.bindAttrib("vertexRectMaskRadii", result.rectMaskRadii.buffer) result.rectMaskShader.bindAttrib("vertexRectMaskMatX", result.rectMaskMatX.buffer) result.rectMaskShader.bindAttrib("vertexRectMaskMatY", result.rectMaskMatY.buffer) + when fastRectMaskLimit >= 2: + result.rectMaskShader.bindAttrib( + "vertexRectMaskParams2", result.rectMaskParams2.buffer + ) + result.rectMaskShader.bindAttrib( + "vertexRectMaskRadii2", result.rectMaskRadii2.buffer + ) + result.rectMaskShader.bindAttrib("vertexRectMaskMat2", result.rectMaskMat2.buffer) # Mask shader. glGenVertexArrays(1, result.maskVertexArrayId.addr) @@ -469,7 +507,6 @@ proc newContext*( result.maskShader.bindAttrib("vertexSdfParams", result.sdfParams.buffer) result.maskShader.bindAttrib("vertexSdfRadii", result.sdfRadii.buffer) result.maskShader.bindAttrib("vertexSdfMode", result.sdfModeAttr.buffer) - result.maskShader.bindAttrib("vertexSubpixelShift", result.subpixelShifts.buffer) # Fullscreen triangle buffers for blur passes. result.blurPositions.componentType = cGL_FLOAT @@ -703,9 +740,6 @@ proc setVert2(buf: var seq[float32], i: int, v: Vec2) = buf[i * 2 + 0] = v.x buf[i * 2 + 1] = v.y -proc setVert1(buf: var seq[float32], i: int, v: float32) = - buf[i] = v - proc setVert4(buf: var seq[float32], i: int, v: Vec4) = buf[i * 4 + 0] = v.x buf[i * 4 + 1] = v.y @@ -750,12 +784,12 @@ proc activeSubpixelShift(ctx: OpenGlContext): float32 = return 0.0'f32 max(0.0'f32, min(ctx.textSubpixelShift, 0.999'f32)) -proc setQuadSubpixelShift(ctx: OpenGlContext, offset: int) = - let shift = ctx.activeSubpixelShift() - ctx.subpixelShifts.data.setVert1(offset + 0, shift) - ctx.subpixelShifts.data.setVert1(offset + 1, shift) - ctx.subpixelShifts.data.setVert1(offset + 2, shift) - ctx.subpixelShifts.data.setVert1(offset + 3, shift) +proc setQuadSdfFactors(ctx: OpenGlContext, offset: int, factors: Vec2) = + let packed = vec4(factors.x, factors.y, ctx.activeSubpixelShift(), 0.0'f32) + ctx.sdfFactors.data.setVert4(offset + 0, packed) + ctx.sdfFactors.data.setVert4(offset + 1, packed) + ctx.sdfFactors.data.setVert4(offset + 2, packed) + ctx.sdfFactors.data.setVert4(offset + 3, packed) proc makeRectMask( ctx: OpenGlContext, maskRect: Rect, radii: array[DirectionCorners, float32] @@ -781,6 +815,13 @@ proc setRectMaskVert4( ctx.rectMaskMatX.data.setVert4(offset + i, matX) ctx.rectMaskMatY.data.setVert4(offset + i, matY) +when fastRectMaskLimit >= 2: + proc setRectMask2Vert4(ctx: OpenGlContext, offset: int, params, radii, mat2: Vec4) = + for i in 0 ..< 4: + ctx.rectMaskParams2.data.setVert4(offset + i, params) + ctx.rectMaskRadii2.data.setVert4(offset + i, radii) + ctx.rectMaskMat2.data.setVert4(offset + i, mat2) + proc setDisabledRectMaskVerts(ctx: OpenGlContext, firstVertex, vertexCount: int) = let params = vec4(0.0'f32, 0.0'f32, -1.0'f32, -1.0'f32) @@ -790,40 +831,77 @@ proc setDisabledRectMaskVerts(ctx: OpenGlContext, firstVertex, vertexCount: int) ctx.rectMaskRadii.data.setVert4(i, zero4) ctx.rectMaskMatX.data.setVert4(i, zero4) ctx.rectMaskMatY.data.setVert4(i, zero4) + when fastRectMaskLimit >= 2: + ctx.rectMaskParams2.data.setVert4(i, params) + ctx.rectMaskRadii2.data.setVert4(i, zero4) + ctx.rectMaskMat2.data.setVert4(i, zero4) + +proc fastRectMaskCount(ctx: OpenGlContext): int = + for rectMask in ctx.rectMaskStack: + if rectMask.kind == rmkFast: + inc result proc setRectMaskVert4(ctx: OpenGlContext, offset: int) = - if ctx.maskBegun: + when fastRectMaskLimit == 0: return - - var - hasRectMask = false - params = vec4(0.0'f32) - radii = vec4(0.0'f32) - matX = vec4(0.0'f32) - matY = vec4(0.0'f32) - - if ctx.rectMaskStack.len > 0: - for i in countdown(ctx.rectMaskStack.len - 1, 0): - let rectMask = ctx.rectMaskStack[i] - if rectMask.kind == rmkFast: - hasRectMask = true - params = rectMask.params - radii = rectMask.radii - matX = rectMask.matX - matY = rectMask.matY - break - - if hasRectMask: - if not ctx.batchHasRectMask: - ctx.setDisabledRectMaskVerts(0, offset) - ctx.batchHasRectMask = true - ctx.setRectMaskVert4(offset, params, radii, matX, matY) - elif ctx.batchHasRectMask: - ctx.setDisabledRectMaskVerts(offset, 4) + else: + if ctx.maskBegun: + return + + var + maskCount = 0 + params = vec4(0.0'f32) + radii = vec4(0.0'f32) + matX = vec4(0.0'f32) + matY = vec4(0.0'f32) + when fastRectMaskLimit >= 2: + var + params2 = vec4(0.0'f32) + radii2 = vec4(0.0'f32) + mat2 = vec4(0.0'f32) + + if ctx.rectMaskStack.len > 0: + for i in countdown(ctx.rectMaskStack.len - 1, 0): + let rectMask = ctx.rectMaskStack[i] + if rectMask.kind == rmkFast: + if maskCount == 0: + params = rectMask.params + radii = rectMask.radii + matX = rectMask.matX + matY = rectMask.matY + inc maskCount + elif fastRectMaskLimit >= 2 and maskCount == 1: + when fastRectMaskLimit >= 2: + params2 = rectMask.params + radii2 = rectMask.radii + mat2 = + vec4(rectMask.matX.x, rectMask.matX.y, rectMask.matX.z, rectMask.matY.x) + matX.w = rectMask.matY.y + matY.w = rectMask.matY.z + inc maskCount + if maskCount >= fastRectMaskLimit: + break + + if maskCount > 0: + if not ctx.batchHasRectMask: + ctx.setDisabledRectMaskVerts(0, offset) + ctx.batchHasRectMask = true + ctx.setRectMaskVert4(offset, params, radii, matX, matY) + when fastRectMaskLimit >= 2: + if maskCount >= 2: + ctx.setRectMask2Vert4(offset, params2, radii2, mat2) + else: + let + disabledParams = vec4(0.0'f32, 0.0'f32, -1.0'f32, -1.0'f32) + zero4 = vec4(0.0'f32) + ctx.setRectMask2Vert4(offset, disabledParams, zero4, zero4) + elif ctx.batchHasRectMask: + ctx.setDisabledRectMaskVerts(offset, 4) template setRectMaskVert4IfNeeded(ctx: OpenGlContext, offset: int) = - if not ctx.maskBegun and (ctx.batchHasRectMask or ctx.rectMaskStack.len > 0): - ctx.setRectMaskVert4(offset) + when fastRectMaskLimit > 0: + if not ctx.maskBegun and (ctx.batchHasRectMask or ctx.rectMaskStack.len > 0): + ctx.setRectMaskVert4(offset) func `*`*(m: Mat4, v: Vec2): Vec2 = (m * vec3(v.x, v.y, 0.0)).xy @@ -864,10 +942,7 @@ proc drawQuad*( ctx.sdfRadii.data.setVert4(offset + 3, zero4) let defaultFactors = vec2(0.0'f32, 0.0'f32) - ctx.sdfFactors.data.setVert2(offset + 0, defaultFactors) - ctx.sdfFactors.data.setVert2(offset + 1, defaultFactors) - ctx.sdfFactors.data.setVert2(offset + 2, defaultFactors) - ctx.sdfFactors.data.setVert2(offset + 3, defaultFactors) + ctx.setQuadSdfFactors(offset, defaultFactors) # atlas fragment mode when defined(emscripten): @@ -878,7 +953,6 @@ proc drawQuad*( ctx.sdfModeAttr.data[offset + 1] = modeVal ctx.sdfModeAttr.data[offset + 2] = modeVal ctx.sdfModeAttr.data[offset + 3] = modeVal - ctx.setQuadSubpixelShift(offset) ctx.setRectMaskVert4IfNeeded(offset) inc ctx.quadCount @@ -971,10 +1045,7 @@ proc drawUvRectAtlasSdf( ctx.sdfRadii.data.setVert4(offset + 2, zero4) ctx.sdfRadii.data.setVert4(offset + 3, zero4) - ctx.sdfFactors.data.setVert2(offset + 0, factors) - ctx.sdfFactors.data.setVert2(offset + 1, factors) - ctx.sdfFactors.data.setVert2(offset + 2, factors) - ctx.sdfFactors.data.setVert2(offset + 3, factors) + ctx.setQuadSdfFactors(offset, factors) when defined(emscripten): let modeVal = mode.int.float32 @@ -984,7 +1055,6 @@ proc drawUvRectAtlasSdf( ctx.sdfModeAttr.data[offset + 1] = modeVal ctx.sdfModeAttr.data[offset + 2] = modeVal ctx.sdfModeAttr.data[offset + 3] = modeVal - ctx.setQuadSubpixelShift(offset) ctx.setRectMaskVert4IfNeeded(offset) inc ctx.quadCount @@ -1105,10 +1175,7 @@ proc drawUvRect(ctx: OpenGlContext, at, to: Vec2, uvAt, uvTo: Vec2, color: Color ctx.sdfRadii.data.setVert4(offset + 3, zero4) let defaultFactors = vec2(0.0'f32, 0.0'f32) - ctx.sdfFactors.data.setVert2(offset + 0, defaultFactors) - ctx.sdfFactors.data.setVert2(offset + 1, defaultFactors) - ctx.sdfFactors.data.setVert2(offset + 2, defaultFactors) - ctx.sdfFactors.data.setVert2(offset + 3, defaultFactors) + ctx.setQuadSdfFactors(offset, defaultFactors) when defined(emscripten): let modeVal = 0.0'f32 @@ -1118,7 +1185,6 @@ proc drawUvRect(ctx: OpenGlContext, at, to: Vec2, uvAt, uvTo: Vec2, color: Color ctx.sdfModeAttr.data[offset + 1] = modeVal ctx.sdfModeAttr.data[offset + 2] = modeVal ctx.sdfModeAttr.data[offset + 3] = modeVal - ctx.setQuadSubpixelShift(offset) ctx.setRectMaskVert4IfNeeded(offset) inc ctx.quadCount @@ -1173,10 +1239,7 @@ proc drawUvRect( ctx.sdfRadii.data.setVert4(offset + 3, zero4) let defaultFactors = vec2(0.0'f32, 0.0'f32) - ctx.sdfFactors.data.setVert2(offset + 0, defaultFactors) - ctx.sdfFactors.data.setVert2(offset + 1, defaultFactors) - ctx.sdfFactors.data.setVert2(offset + 2, defaultFactors) - ctx.sdfFactors.data.setVert2(offset + 3, defaultFactors) + ctx.setQuadSdfFactors(offset, defaultFactors) when defined(emscripten): let modeVal = 0.0'f32 @@ -1186,7 +1249,6 @@ proc drawUvRect( ctx.sdfModeAttr.data[offset + 1] = modeVal ctx.sdfModeAttr.data[offset + 2] = modeVal ctx.sdfModeAttr.data[offset + 3] = modeVal - ctx.setQuadSubpixelShift(offset) ctx.setRectMaskVert4IfNeeded(offset) inc ctx.quadCount @@ -1429,10 +1491,7 @@ proc drawRoundedRectSdfOpenGl( vec2(factor, spread) else: vec2(factor, clamp(fillMidPos, 0.01'f32, 0.99'f32)) - ctx.sdfFactors.data.setVert2(offset + 0, factors) - ctx.sdfFactors.data.setVert2(offset + 1, factors) - ctx.sdfFactors.data.setVert2(offset + 2, factors) - ctx.sdfFactors.data.setVert2(offset + 3, factors) + ctx.setQuadSdfFactors(offset, factors) when defined(emscripten): let modeVal = encodeSdfMode(mode, fillMode) @@ -1442,7 +1501,6 @@ proc drawRoundedRectSdfOpenGl( ctx.sdfModeAttr.data[offset + 1] = modeVal ctx.sdfModeAttr.data[offset + 2] = modeVal ctx.sdfModeAttr.data[offset + 3] = modeVal - ctx.setQuadSubpixelShift(offset) ctx.setRectMaskVert4IfNeeded(offset) inc ctx.quadCount @@ -1703,7 +1761,8 @@ method beginRectMask*( assert ctx.frameBegun == true, "ctx.beginFrame has not been called." assert ctx.maskBegun == false, "ctx.beginRectMask cannot start inside a mask." - if ctx.rectMaskStack.len == 0 and maskRect.w > 0.0'f32 and maskRect.h > 0.0'f32: + if fastRectMaskLimit > 0 and ctx.fastRectMaskCount() < fastRectMaskLimit and + maskRect.w > 0.0'f32 and maskRect.h > 0.0'f32: ctx.rectMaskStack.add(ctx.makeRectMask(maskRect, radii)) else: ctx.beginMask(maskRect, radii) diff --git a/src/figdraw/opengl/glsl/atlas.frag b/src/figdraw/opengl/glsl/atlas.frag index b678c617..69a3446e 100644 --- a/src/figdraw/opengl/glsl/atlas.frag +++ b/src/figdraw/opengl/glsl/atlas.frag @@ -8,8 +8,7 @@ in vec4 fillStopColor; in vec4 sdfParams; in vec4 sdfRadii; in float sdfMode; -in vec2 sdfFactors; -in float subpixelShift; +in vec4 sdfFactors; uniform vec2 windowFrame; uniform sampler2D atlasTex; @@ -130,7 +129,7 @@ void main() { if (sdfModeInt == sdfModeAtlas) { vec2 atlasUv = uv; if (subpixelPositioningEnabled) { - atlasUv.x -= subpixelShift * atlasTexelSize.x; + atlasUv.x -= sdfFactors.z * atlasTexelSize.x; } vec4 tex = texture(atlasTex, atlasUv); fragColor = vec4( diff --git a/src/figdraw/opengl/glsl/atlas.vert b/src/figdraw/opengl/glsl/atlas.vert index f80cf7cf..90a747c8 100644 --- a/src/figdraw/opengl/glsl/atlas.vert +++ b/src/figdraw/opengl/glsl/atlas.vert @@ -8,8 +8,7 @@ in vec4 vertexFillStopColor; in vec4 vertexSdfParams; in vec4 vertexSdfRadii; in float vertexSdfMode; -in vec2 vertexSdfFactors; -in float vertexSubpixelShift; +in vec4 vertexSdfFactors; uniform mat4 proj; @@ -21,8 +20,7 @@ out vec4 fillStopColor; out vec4 sdfParams; out vec4 sdfRadii; out float sdfMode; -out vec2 sdfFactors; -out float subpixelShift; +out vec4 sdfFactors; void main() { pos = vertexPos; @@ -34,6 +32,5 @@ void main() { sdfRadii = vertexSdfRadii; sdfMode = vertexSdfMode; sdfFactors = vertexSdfFactors; - subpixelShift = vertexSubpixelShift; gl_Position = proj * vec4(vertexPos.x, vertexPos.y, 0.0, 1.0); } diff --git a/src/figdraw/opengl/glsl/atlas_rect_mask.frag b/src/figdraw/opengl/glsl/atlas_rect_mask.frag index dee04647..aa94cd1f 100644 --- a/src/figdraw/opengl/glsl/atlas_rect_mask.frag +++ b/src/figdraw/opengl/glsl/atlas_rect_mask.frag @@ -8,12 +8,16 @@ in vec4 fillStopColor; in vec4 sdfParams; in vec4 sdfRadii; in float sdfMode; -in vec2 sdfFactors; -in float subpixelShift; +in vec4 sdfFactors; in vec4 rectMaskParams; in vec4 rectMaskRadii; in vec4 rectMaskMatX; in vec4 rectMaskMatY; +#if FIGDRAW_FAST_RECT_MASK_LIMIT >= 2 +in vec4 rectMaskParams2; +in vec4 rectMaskRadii2; +in vec4 rectMaskMat2; +#endif uniform vec2 windowFrame; uniform sampler2D atlasTex; @@ -76,20 +80,35 @@ float shadowProfile(float sd, float blurRadius) { return exp(-0.5 * z * z); } -float rectMaskAlpha(vec2 pixelPos) { - if (rectMaskParams.z < 0.0 || rectMaskParams.w < 0.0) { +float rectMaskAlphaOne( + vec2 pixelPos, + vec4 params, + vec4 radii, + vec4 matX, + vec4 matY) { + if (params.z < 0.0 || params.w < 0.0) { return 1.0; } vec2 local = vec2( - dot(rectMaskMatX.xy, pixelPos) + rectMaskMatX.z, - dot(rectMaskMatY.xy, pixelPos) + rectMaskMatY.z + dot(matX.xy, pixelPos) + matX.z, + dot(matY.xy, pixelPos) + matY.z ); - vec2 q = local - rectMaskParams.xy; - float dist = sdRoundedBox(vec2(q.x, -q.y), rectMaskParams.zw, rectMaskRadii); + vec2 q = local - params.xy; + float dist = sdRoundedBox(vec2(q.x, -q.y), params.zw, radii); return 1.0 - clamp(aaFactor * dist + 0.5, 0.0, 1.0); } +float rectMaskAlpha(vec2 pixelPos) { + float alpha = + rectMaskAlphaOne(pixelPos, rectMaskParams, rectMaskRadii, rectMaskMatX, rectMaskMatY); +#if FIGDRAW_FAST_RECT_MASK_LIMIT >= 2 + vec4 matY2 = vec4(rectMaskMat2.w, rectMaskMatX.w, rectMaskMatY.w, 0.0); + alpha *= rectMaskAlphaOne(pixelPos, rectMaskParams2, rectMaskRadii2, rectMaskMat2, matY2); +#endif + return alpha; +} + float linear3T(int fillMode, vec2 uv) { switch (fillMode) { case 1: @@ -148,7 +167,7 @@ void main() { if (sdfModeInt == sdfModeAtlas) { vec2 atlasUv = uv; if (subpixelPositioningEnabled) { - atlasUv.x -= subpixelShift * atlasTexelSize.x; + atlasUv.x -= sdfFactors.z * atlasTexelSize.x; } vec4 tex = texture(atlasTex, atlasUv); fragColor = vec4( diff --git a/src/figdraw/opengl/glsl/atlas_rect_mask.vert b/src/figdraw/opengl/glsl/atlas_rect_mask.vert index 7921641b..3310ad18 100644 --- a/src/figdraw/opengl/glsl/atlas_rect_mask.vert +++ b/src/figdraw/opengl/glsl/atlas_rect_mask.vert @@ -8,12 +8,16 @@ in vec4 vertexFillStopColor; in vec4 vertexSdfParams; in vec4 vertexSdfRadii; in float vertexSdfMode; -in vec2 vertexSdfFactors; -in float vertexSubpixelShift; +in vec4 vertexSdfFactors; in vec4 vertexRectMaskParams; in vec4 vertexRectMaskRadii; in vec4 vertexRectMaskMatX; in vec4 vertexRectMaskMatY; +#if FIGDRAW_FAST_RECT_MASK_LIMIT >= 2 +in vec4 vertexRectMaskParams2; +in vec4 vertexRectMaskRadii2; +in vec4 vertexRectMaskMat2; +#endif uniform mat4 proj; @@ -25,12 +29,16 @@ out vec4 fillStopColor; out vec4 sdfParams; out vec4 sdfRadii; out float sdfMode; -out vec2 sdfFactors; -out float subpixelShift; +out vec4 sdfFactors; out vec4 rectMaskParams; out vec4 rectMaskRadii; out vec4 rectMaskMatX; out vec4 rectMaskMatY; +#if FIGDRAW_FAST_RECT_MASK_LIMIT >= 2 +out vec4 rectMaskParams2; +out vec4 rectMaskRadii2; +out vec4 rectMaskMat2; +#endif void main() { pos = vertexPos; @@ -42,10 +50,14 @@ void main() { sdfRadii = vertexSdfRadii; sdfMode = vertexSdfMode; sdfFactors = vertexSdfFactors; - subpixelShift = vertexSubpixelShift; rectMaskParams = vertexRectMaskParams; rectMaskRadii = vertexRectMaskRadii; rectMaskMatX = vertexRectMaskMatX; rectMaskMatY = vertexRectMaskMatY; +#if FIGDRAW_FAST_RECT_MASK_LIMIT >= 2 + rectMaskParams2 = vertexRectMaskParams2; + rectMaskRadii2 = vertexRectMaskRadii2; + rectMaskMat2 = vertexRectMaskMat2; +#endif gl_Position = proj * vec4(vertexPos.x, vertexPos.y, 0.0, 1.0); } diff --git a/src/figdraw/opengl/glsl/emscripten/atlas.frag b/src/figdraw/opengl/glsl/emscripten/atlas.frag index c5c6ba10..941bf064 100644 --- a/src/figdraw/opengl/glsl/emscripten/atlas.frag +++ b/src/figdraw/opengl/glsl/emscripten/atlas.frag @@ -11,8 +11,7 @@ varying vec4 fillStopColor; varying vec4 sdfParams; varying vec4 sdfRadii; varying float sdfMode; -varying vec2 sdfFactors; -varying float subpixelShift; +varying vec4 sdfFactors; uniform vec2 windowFrame; uniform sampler2D atlasTex; @@ -132,7 +131,7 @@ void main() { if (sdfModeInt == sdfModeAtlas) { vec2 atlasUv = uv; if (subpixelPositioningEnabled) { - atlasUv.x -= subpixelShift * atlasTexelSize.x; + atlasUv.x -= sdfFactors.z * atlasTexelSize.x; } vec4 tex = texture2D(atlasTex, atlasUv); fragColor = vec4( diff --git a/src/figdraw/opengl/glsl/emscripten/atlas.vert b/src/figdraw/opengl/glsl/emscripten/atlas.vert index 64066190..f032bcbf 100644 --- a/src/figdraw/opengl/glsl/emscripten/atlas.vert +++ b/src/figdraw/opengl/glsl/emscripten/atlas.vert @@ -10,8 +10,7 @@ attribute vec4 vertexFillStopColor; attribute vec4 vertexSdfParams; attribute vec4 vertexSdfRadii; attribute float vertexSdfMode; -attribute vec2 vertexSdfFactors; -attribute float vertexSubpixelShift; +attribute vec4 vertexSdfFactors; uniform mat4 proj; @@ -23,8 +22,7 @@ varying vec4 fillStopColor; varying vec4 sdfParams; varying vec4 sdfRadii; varying float sdfMode; -varying vec2 sdfFactors; -varying float subpixelShift; +varying vec4 sdfFactors; void main() { pos = vertexPos; @@ -36,6 +34,5 @@ void main() { sdfRadii = vertexSdfRadii; sdfMode = vertexSdfMode; sdfFactors = vertexSdfFactors; - subpixelShift = vertexSubpixelShift; gl_Position = proj * vec4(vertexPos.x, vertexPos.y, 0.0, 1.0); } diff --git a/src/figdraw/opengl/glsl/emscripten/atlas_rect_mask.frag b/src/figdraw/opengl/glsl/emscripten/atlas_rect_mask.frag index 488bfe7d..643fde80 100644 --- a/src/figdraw/opengl/glsl/emscripten/atlas_rect_mask.frag +++ b/src/figdraw/opengl/glsl/emscripten/atlas_rect_mask.frag @@ -11,12 +11,16 @@ varying vec4 fillStopColor; varying vec4 sdfParams; varying vec4 sdfRadii; varying float sdfMode; -varying vec2 sdfFactors; -varying float subpixelShift; +varying vec4 sdfFactors; varying vec4 rectMaskParams; varying vec4 rectMaskRadii; varying vec4 rectMaskMatX; varying vec4 rectMaskMatY; +#if FIGDRAW_FAST_RECT_MASK_LIMIT >= 2 +varying vec4 rectMaskParams2; +varying vec4 rectMaskRadii2; +varying vec4 rectMaskMat2; +#endif uniform vec2 windowFrame; uniform sampler2D atlasTex; @@ -78,20 +82,35 @@ float shadowProfile(float sd, float blurRadius) { return exp(-0.5 * z * z); } -float rectMaskAlpha(vec2 pixelPos) { - if (rectMaskParams.z < 0.0 || rectMaskParams.w < 0.0) { +float rectMaskAlphaOne( + vec2 pixelPos, + vec4 params, + vec4 radii, + vec4 matX, + vec4 matY) { + if (params.z < 0.0 || params.w < 0.0) { return 1.0; } vec2 local = vec2( - dot(rectMaskMatX.xy, pixelPos) + rectMaskMatX.z, - dot(rectMaskMatY.xy, pixelPos) + rectMaskMatY.z + dot(matX.xy, pixelPos) + matX.z, + dot(matY.xy, pixelPos) + matY.z ); - vec2 q = local - rectMaskParams.xy; - float dist = sdRoundedBox(vec2(q.x, -q.y), rectMaskParams.zw, rectMaskRadii); + vec2 q = local - params.xy; + float dist = sdRoundedBox(vec2(q.x, -q.y), params.zw, radii); return 1.0 - clamp(aaFactor * dist + 0.5, 0.0, 1.0); } +float rectMaskAlpha(vec2 pixelPos) { + float alpha = + rectMaskAlphaOne(pixelPos, rectMaskParams, rectMaskRadii, rectMaskMatX, rectMaskMatY); +#if FIGDRAW_FAST_RECT_MASK_LIMIT >= 2 + vec4 matY2 = vec4(rectMaskMat2.w, rectMaskMatX.w, rectMaskMatY.w, 0.0); + alpha *= rectMaskAlphaOne(pixelPos, rectMaskParams2, rectMaskRadii2, rectMaskMat2, matY2); +#endif + return alpha; +} + float linear3T(int fillMode, vec2 uv) { if (fillMode == 1) { return uv.x; @@ -150,7 +169,7 @@ void main() { if (sdfModeInt == sdfModeAtlas) { vec2 atlasUv = uv; if (subpixelPositioningEnabled) { - atlasUv.x -= subpixelShift * atlasTexelSize.x; + atlasUv.x -= sdfFactors.z * atlasTexelSize.x; } vec4 tex = texture2D(atlasTex, atlasUv); fragColor = vec4( diff --git a/src/figdraw/opengl/glsl/emscripten/atlas_rect_mask.vert b/src/figdraw/opengl/glsl/emscripten/atlas_rect_mask.vert index 7ffdb6f4..f4636fd7 100644 --- a/src/figdraw/opengl/glsl/emscripten/atlas_rect_mask.vert +++ b/src/figdraw/opengl/glsl/emscripten/atlas_rect_mask.vert @@ -10,12 +10,16 @@ attribute vec4 vertexFillStopColor; attribute vec4 vertexSdfParams; attribute vec4 vertexSdfRadii; attribute float vertexSdfMode; -attribute vec2 vertexSdfFactors; -attribute float vertexSubpixelShift; +attribute vec4 vertexSdfFactors; attribute vec4 vertexRectMaskParams; attribute vec4 vertexRectMaskRadii; attribute vec4 vertexRectMaskMatX; attribute vec4 vertexRectMaskMatY; +#if FIGDRAW_FAST_RECT_MASK_LIMIT >= 2 +attribute vec4 vertexRectMaskParams2; +attribute vec4 vertexRectMaskRadii2; +attribute vec4 vertexRectMaskMat2; +#endif uniform mat4 proj; @@ -27,12 +31,16 @@ varying vec4 fillStopColor; varying vec4 sdfParams; varying vec4 sdfRadii; varying float sdfMode; -varying vec2 sdfFactors; -varying float subpixelShift; +varying vec4 sdfFactors; varying vec4 rectMaskParams; varying vec4 rectMaskRadii; varying vec4 rectMaskMatX; varying vec4 rectMaskMatY; +#if FIGDRAW_FAST_RECT_MASK_LIMIT >= 2 +varying vec4 rectMaskParams2; +varying vec4 rectMaskRadii2; +varying vec4 rectMaskMat2; +#endif void main() { pos = vertexPos; @@ -44,10 +52,14 @@ void main() { sdfRadii = vertexSdfRadii; sdfMode = vertexSdfMode; sdfFactors = vertexSdfFactors; - subpixelShift = vertexSubpixelShift; rectMaskParams = vertexRectMaskParams; rectMaskRadii = vertexRectMaskRadii; rectMaskMatX = vertexRectMaskMatX; rectMaskMatY = vertexRectMaskMatY; +#if FIGDRAW_FAST_RECT_MASK_LIMIT >= 2 + rectMaskParams2 = vertexRectMaskParams2; + rectMaskRadii2 = vertexRectMaskRadii2; + rectMaskMat2 = vertexRectMaskMat2; +#endif gl_Position = proj * vec4(vertexPos.x, vertexPos.y, 0.0, 1.0); } diff --git a/src/figdraw/opengl/glsl/emscripten/mask.frag b/src/figdraw/opengl/glsl/emscripten/mask.frag index 4d94270f..befcca03 100644 --- a/src/figdraw/opengl/glsl/emscripten/mask.frag +++ b/src/figdraw/opengl/glsl/emscripten/mask.frag @@ -8,7 +8,6 @@ varying vec4 color; varying vec4 sdfParams; varying vec4 sdfRadii; varying float sdfMode; -varying float subpixelShift; uniform vec2 windowFrame; uniform sampler2D atlasTex; diff --git a/src/figdraw/opengl/glsl/mask.frag b/src/figdraw/opengl/glsl/mask.frag index 674ebb27..0a30f451 100644 --- a/src/figdraw/opengl/glsl/mask.frag +++ b/src/figdraw/opengl/glsl/mask.frag @@ -6,7 +6,6 @@ in vec4 color; in vec4 sdfParams; in vec4 sdfRadii; in float sdfMode; -in float subpixelShift; uniform vec2 windowFrame; uniform sampler2D atlasTex; diff --git a/src/figdraw/opengl/shaders.nim b/src/figdraw/opengl/shaders.nim index 7318319a..98d5c8a6 100644 --- a/src/figdraw/opengl/shaders.nim +++ b/src/figdraw/opengl/shaders.nim @@ -225,6 +225,17 @@ proc newShader*(vertPath, fragPath: string): Shader = fragPathFull = dir / fragPath newShader((vertPathFull, vertCode), (fragPathFull, fragCode)) +func injectGlslDefines(source, defines: string): string = + if defines.len == 0: + return source + + if source.startsWith("#version"): + let lineEnd = source.find('\n') + if lineEnd >= 0: + return source[0 .. lineEnd] & defines & source[lineEnd + 1 ..^ 1] + + defines & source + template newShaderStatic*(vertPath, fragPath: string): Shader = ## Creates a new shader but also statically reads vertPath and fragPath ## so they are compiled into the binary. @@ -236,6 +247,22 @@ template newShaderStatic*(vertPath, fragPath: string): Shader = fragPathFull = dir / fragPath newShader((vertPathFull, vertCode), (fragPathFull, fragCode)) +template newShaderStaticWithDefines*( + vertPath, fragPath: string, defines: static[string] +): Shader = + ## Creates a new shader from static sources with GLSL defines inserted after + ## the #version line. + const + vertCode = staticRead(vertPath) + fragCode = staticRead(fragPath) + dir = getProjectPath() + vertPathFull = dir / vertPath + fragPathFull = dir / fragPath + newShader( + (vertPathFull, injectGlslDefines(vertCode, defines)), + (fragPathFull, injectGlslDefines(fragCode, defines)), + ) + proc hasUniform*(shader: Shader, name: string): bool = for uniform in shader.uniforms: if uniform.name == name: @@ -413,8 +440,7 @@ proc bindAttrib*(shader: Shader, name: string, buffer: Buffer) = # integer data. Other scalar formats (e.g. u16 enum values) should flow # through the float attribute path for GLSL ES compatibility. let useIntegerPointer = - not buffer.normalized and - buffer.kind == bkSCALAR and + not buffer.normalized and buffer.kind == bkSCALAR and (buffer.componentType == cGL_INT or buffer.componentType == GL_UNSIGNED_INT) if not useIntegerPointer: diff --git a/src/figdraw/vulkan/shaders/sdf.frag.glsl b/src/figdraw/vulkan/shaders/sdf.frag.glsl index a4ce2c4b..dcbc0013 100644 --- a/src/figdraw/vulkan/shaders/sdf.frag.glsl +++ b/src/figdraw/vulkan/shaders/sdf.frag.glsl @@ -23,6 +23,12 @@ layout(location = 9) in vec4 vRectMaskParams; layout(location = 10) in vec4 vRectMaskRadii; layout(location = 11) in vec4 vRectMaskMatX; layout(location = 12) in vec4 vRectMaskMatY; +#if FIGDRAW_FAST_RECT_MASK_LIMIT >= 2 +layout(location = 13) in vec4 vRectMaskParams2; +layout(location = 14) in vec4 vRectMaskRadii2; +layout(location = 15) in vec4 vRectMaskMatX2; +layout(location = 16) in vec4 vRectMaskMatY2; +#endif layout(location = 0) out vec4 fragColor; @@ -61,20 +67,36 @@ float sdRoundedBox(vec2 p, vec2 b, vec4 r) { return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr; } -float rectMaskAlpha(vec2 pixelPos) { - if (vRectMaskParams.z <= 0.0 || vRectMaskParams.w <= 0.0) { +float rectMaskAlphaOne( + vec2 pixelPos, + vec4 params, + vec4 radii, + vec4 matX, + vec4 matY) { + if (params.z <= 0.0 || params.w <= 0.0) { return 1.0; } vec2 local = vec2( - dot(vRectMaskMatX.xy, pixelPos) + vRectMaskMatX.z, - dot(vRectMaskMatY.xy, pixelPos) + vRectMaskMatY.z + dot(matX.xy, pixelPos) + matX.z, + dot(matY.xy, pixelPos) + matY.z ); - vec2 q = local - vRectMaskParams.xy; - float dist = sdRoundedBox(vec2(q.x, -q.y), vRectMaskParams.zw, vRectMaskRadii); + vec2 q = local - params.xy; + float dist = sdRoundedBox(vec2(q.x, -q.y), params.zw, radii); return 1.0 - clamp(uFS.aaFactor * dist + 0.5, 0.0, 1.0); } +float rectMaskAlpha(vec2 pixelPos) { + float alpha = + rectMaskAlphaOne(pixelPos, vRectMaskParams, vRectMaskRadii, vRectMaskMatX, vRectMaskMatY); +#if FIGDRAW_FAST_RECT_MASK_LIMIT >= 2 + alpha *= rectMaskAlphaOne( + pixelPos, vRectMaskParams2, vRectMaskRadii2, vRectMaskMatX2, vRectMaskMatY2 + ); +#endif + return alpha; +} + float shadowProfile(float sd, float blurRadius) { // CSS-like calibration: sigma ~= blurRadius / 2 float sigma = max(0.5 * blurRadius, 0.5); diff --git a/src/figdraw/vulkan/shaders/sdf.frag.spv b/src/figdraw/vulkan/shaders/sdf.frag.spv index a00c737069971dfd30b3ab9465793b93944e2312..6c1c2c8f7add053cc7fd97fc442c04d99092dfce 100644 GIT binary patch literal 19996 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 W7}o*l{3*v}pY5mqj!T>O* literal 0 HcmV?d00001 diff --git a/src/figdraw/vulkan/shaders/sdf.rectmask0.vert.spv b/src/figdraw/vulkan/shaders/sdf.rectmask0.vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..3daa9abd3ceb96696828ad63d210d73908f5b86e GIT binary patch literal 3200 zcmZXVTT>H35QP`QCU_SVMMYf1JBoNiyn>>DN;DFo^vOz*XsyO9m8>bt7yS$VC4ZGK zR(ZbJnazNkYGzKK?%AH1(@UX$vJ?b;!Jc3+SP!yiDCn0bkgsUQ<<;uybSJG(-@G+L z#QvaQ0*N^gL}osqzh7!`3$RPJN48J4Up6c|EIT4QDmx}SE*q1bl%0}AvNN)CvJ0|{ zvMJeR*|hAc?3(Pl?56CNY)w|suV3SD6%z&n`t|AnQ%kp!bTo($OoaMb-2gwT4OUU*E*kV;i2@#-DWvENSnvK z=XrUpC8LGyc^S$6&`6$BuBC6yS|y{K?Kv4q4tnx)-q{>9_Fs-jG&3h|%fyAjU5y*n z^;Y~LX>%tqb@cVo=fiWAlBb(cUm>B&KNn%;AzwoW@B@-ja!z z#4~@~N~`gAb5D+=2b8nejJKY|owU~4Y~*=#E8N#+Gv}y>{^41+eKMXwgXv!%(<5MN zAf7()@vz0z0Z)&>J4}CosRf?zfRE_~F#QY9_rb^L9voU8&HEUwgNv4rw&8-&H#M_1 zS|?T?K7-M@kI^@H(DHnXe2kvKd|Ox#9s3yFf_Gb}!pB~~ z^a4K4?t#Wg#@bUBKh&Iu{CSPUvK~CTS(NcO{KTN2^I*>OnT++wf3DH#drbJ1Fl!Jq zu31GRwc~@s$a}4k{lil?c+L7|)sNJ~etC%dBM;vMVmTlFU-8}t!i;=d=#httrkB8u zJD@rJ0Y~3};izjwhSuTe39#cvHAm-g=pXF36PlxOIJ6IT9N#wd4d=e|j+>A_vK*QR z!*Qf{AhGP_;bqLz=&l Qp;PuwJeZij>WDenKh<&!Hvj+t literal 0 HcmV?d00001 diff --git a/src/figdraw/vulkan/shaders/sdf.rectmask1.frag.spv b/src/figdraw/vulkan/shaders/sdf.rectmask1.frag.spv new file mode 100644 index 0000000000000000000000000000000000000000..c6599d9da476291c12e996c084b99c279bf0f93e GIT binary patch literal 19368 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 W7}o*l{3*v}pY5mqj!T>O* literal 0 HcmV?d00001 diff --git a/src/figdraw/vulkan/shaders/sdf.rectmask1.vert.spv b/src/figdraw/vulkan/shaders/sdf.rectmask1.vert.spv new file mode 100644 index 0000000000000000000000000000000000000000..3daa9abd3ceb96696828ad63d210d73908f5b86e GIT binary patch literal 3200 zcmZXVTT>H35QP`QCU_SVMMYf1JBoNiyn>>DN;DFo^vOz*XsyO9m8>bt7yS$VC4ZGK zR(ZbJnazNkYGzKK?%AH1(@UX$vJ?b;!Jc3+SP!yiDCn0bkgsUQ<<;uybSJG(-@G+L z#QvaQ0*N^gL}osqzh7!`3$RPJN48J4Up6c|EIT4QDmx}SE*q1bl%0}AvNN)CvJ0|{ zvMJeR*|hAc?3(Pl?56CNY)w|suV3SD6%z&n`t|AnQ%kp!bTo($OoaMb-2gwT4OUU*E*kV;i2@#-DWvENSnvK z=XrUpC8LGyc^S$6&`6$BuBC6yS|y{K?Kv4q4tnx)-q{>9_Fs-jG&3h|%fyAjU5y*n z^;Y~LX>%tqb@cVo=fiWAlBb(cUm>B&KNn%;AzwoW@B@-ja!z z#4~@~N~`gAb5D+=2b8nejJKY|owU~4Y~*=#E8N#+Gv}y>{^41+eKMXwgXv!%(<5MN zAf7()@vz0z0Z)&>J4}CosRf?zfRE_~F#QY9_rb^L9voU8&HEUwgNv4rw&8-&H#M_1 zS|?T?K7-M@kI^@H(DHnXe2kvKd|Ox#9s3yFf_Gb}!pB~~ z^a4K4?t#Wg#@bUBKh&Iu{CSPUvK~CTS(NcO{KTN2^I*>OnT++wf3DH#drbJ1Fl!Jq zu31GRwc~@s$a}4k{lil?c+L7|)sNJ~etC%dBM;vMVmTlFU-8}t!i;=d=#httrkB8u zJD@rJ0Y~3};izjwhSuTe39#cvHAm-g=pXF36PlxOIJ6IT9N#wd4d=e|j+>A_vK*QR z!*Qf{AhGP_;bqLz=&l Qp;PuwJeZij>WDenKh<&!Hvj+t literal 0 HcmV?d00001 diff --git a/src/figdraw/vulkan/shaders/sdf.vert.glsl b/src/figdraw/vulkan/shaders/sdf.vert.glsl index b9f6af32..4b9075ac 100644 --- a/src/figdraw/vulkan/shaders/sdf.vert.glsl +++ b/src/figdraw/vulkan/shaders/sdf.vert.glsl @@ -17,6 +17,12 @@ layout(location = 9) in vec4 vertexRectMaskParams; layout(location = 10) in vec4 vertexRectMaskRadii; layout(location = 11) in vec4 vertexRectMaskMatX; layout(location = 12) in vec4 vertexRectMaskMatY; +#if FIGDRAW_FAST_RECT_MASK_LIMIT >= 2 +layout(location = 13) in vec4 vertexRectMaskParams2; +layout(location = 14) in vec4 vertexRectMaskRadii2; +layout(location = 15) in vec4 vertexRectMaskMatX2; +layout(location = 16) in vec4 vertexRectMaskMatY2; +#endif layout(location = 0) out vec2 vPos; layout(location = 1) out vec2 vUv; @@ -31,6 +37,12 @@ layout(location = 9) out vec4 vRectMaskParams; layout(location = 10) out vec4 vRectMaskRadii; layout(location = 11) out vec4 vRectMaskMatX; layout(location = 12) out vec4 vRectMaskMatY; +#if FIGDRAW_FAST_RECT_MASK_LIMIT >= 2 +layout(location = 13) out vec4 vRectMaskParams2; +layout(location = 14) out vec4 vRectMaskRadii2; +layout(location = 15) out vec4 vRectMaskMatX2; +layout(location = 16) out vec4 vRectMaskMatY2; +#endif void main() { vPos = vertexPos; @@ -46,6 +58,12 @@ void main() { vRectMaskRadii = vertexRectMaskRadii; vRectMaskMatX = vertexRectMaskMatX; vRectMaskMatY = vertexRectMaskMatY; +#if FIGDRAW_FAST_RECT_MASK_LIMIT >= 2 + vRectMaskParams2 = vertexRectMaskParams2; + vRectMaskRadii2 = vertexRectMaskRadii2; + vRectMaskMatX2 = vertexRectMaskMatX2; + vRectMaskMatY2 = vertexRectMaskMatY2; +#endif vec4 p = uVS.proj * vec4(vertexPos.xy, 0.0, 1.0); p.y = -p.y; diff --git a/src/figdraw/vulkan/shaders/sdf.vert.spv b/src/figdraw/vulkan/shaders/sdf.vert.spv index 1e4dc5e4fcfc135cf0442e19a4d4be47425c66c1..0127216172476c7ae8b94a7050e4c849d8245f68 100644 GIT binary patch literal 3820 zcmZXVeRESq6vi)2Z&F1;KweZVDaDFbD^|o?XJ5~=Eltf=lH^2kGMPzQiT}}A<0*;(1EvJ0|{ve#s<%Pz~_lwFZk zW!Gd2GDdtywkUf~wj}#N_Mz;i?3Qd<_Oa}a>{Ho_?5=E8R+FvC9?44jn^OCa7Mmv1 z`Wvqk%vwi{e4s{r>efB#^oO0_y)#ul`xxzw9M66{FNe#ScKaKx=iR*T$6&8M#yeA; zBYPihX5DT*Yll#cS8?azV<$;+I476W9Og&G_!W0{bIV1SB%3M z`}i?IEay(_ZmXST>JwXyV*^1fcSCGFZ+FynvD{TLZXk%|{)pXd9S-yUK%R=b&mz`{ zFb?-T+U*<;>#f0WVd1Ijk-PUf$H*UVQM%%0T+L4AjRTmEdMyvYJ4&;kZ#J4GHZ72LCKx8PV0Zv)>}cmrlVbeOj*#^@i+8i?l&i1FEgSqD7&hc}G& z!K?+IH!#NN9L(DU&)XPdG!0GzkDgiSTe7awDyA8HXFt@+5oN1d6EK0a#eoGOx-IYr>7e?dL!rYgIrDAo>!s|r&G z#NuBGn7hXZ&popS5T-V$r&*1h&gIrXbDI5Y=_<6PgCEy&=| z6Bv&Dy(>c_aA*f?W4^CG`hY_>VB>D6k2c`Y4A{7j)JGR^=ml)tZS|J}hgQJGeWE^k zAP=2@jr&afmB679uyOaC-{+wZuyLQOzvlQ-Qd8@3>gsbxn3|?;iKkZUGW=l9=W7}F z|3Fnjtvv`nHHB|C_)e&8D2w}VIzCC@sQZTOZ;b)>t&DkyVXxF2zBTdG{dY2a)H>^6 s-iI>$e+GOIe3tXQu;nl&m>4^U$AKrF{}VsU&;@%Z9!$)CQs5og|EMflA^-pY delta 966 zcmYk5&uSA<6voeFW*RXBl^`nG5-Su$TuEu#+CQ<@sTxh1Xk8AH4l)|jkfvhY#20W8 z@&rDD1UK$<vmmJYsNH^fbS>h!v=U>`-d_b(%_LiV&PpAuBUrAbBZ!6;>nZ4sN0oicJc@ZH*Ng2u;4R) zW%9Fd=nSj6EhKKr7z^oZVk5UI+e|SE?#Z~(-Q;sI+fHM0|6>o*80s6+jj})nmmHHb z?MOY88|rmU4}!EV!F;#Vn4gUPSeO{&B8)L>GUCG&KS^T@!*xBCWi|8!!=WBQtu$Y? X1>Y_RJd^QvWO@p~kpAi|RAm1EG3aH2 diff --git a/src/figdraw/vulkan/vulkan_context.nim b/src/figdraw/vulkan/vulkan_context.nim index 03667b6a..5217273c 100644 --- a/src/figdraw/vulkan/vulkan_context.nim +++ b/src/figdraw/vulkan/vulkan_context.nim @@ -22,11 +22,23 @@ logScope: const quadLimit = 10_921 - sdfVertSpv = staticRead("shaders/sdf.vert.spv") - sdfFragSpv = staticRead("shaders/sdf.frag.spv") + fastRectMaskLimit = figbackend.FigDrawFastRectMaskLimit blurVertSpv = staticRead("shaders/blur.vert.spv") blurFragSpv = staticRead("shaders/blur.frag.spv") +when fastRectMaskLimit == 0: + const + sdfVertSpv = staticRead("shaders/sdf.rectmask0.vert.spv") + sdfFragSpv = staticRead("shaders/sdf.rectmask0.frag.spv") +elif fastRectMaskLimit == 1: + const + sdfVertSpv = staticRead("shaders/sdf.rectmask1.vert.spv") + sdfFragSpv = staticRead("shaders/sdf.rectmask1.frag.spv") +else: + const + sdfVertSpv = staticRead("shaders/sdf.vert.spv") + sdfFragSpv = staticRead("shaders/sdf.frag.spv") + when defined(emscripten): type SdfModeData = float32 else: @@ -86,6 +98,11 @@ type rectMaskRadii: array[4, float32] rectMaskMatX: array[4, float32] rectMaskMatY: array[4, float32] + when fastRectMaskLimit >= 2: + rectMaskParams2: array[4, float32] + rectMaskRadii2: array[4, float32] + rectMaskMatX2: array[4, float32] + rectMaskMatY2: array[4, float32] VulkanContext* = ref object of figbackend.BackendContext atlasSize: int @@ -125,6 +142,11 @@ type rectMaskRadii: seq[float32] rectMaskMatX: seq[float32] rectMaskMatY: seq[float32] + when fastRectMaskLimit >= 2: + rectMaskParams2: seq[float32] + rectMaskRadii2: seq[float32] + rectMaskMatX2: seq[float32] + rectMaskMatY2: seq[float32] rectMaskStack: seq[RectMask] batchHasRectMask: bool indices: seq[uint16] @@ -768,86 +790,120 @@ proc createPipeline(ctx: VulkanContext) = binding: 0, stride: uint32(sizeof(Vertex)), inputRate: VK_VERTEX_INPUT_RATE_VERTEX ) - let attrDescs = [ - VkVertexInputAttributeDescription( - location: 0, - binding: 0, - format: VK_FORMAT_R32G32_SFLOAT, - offset: uint32(offsetOf(Vertex, pos)), - ), - VkVertexInputAttributeDescription( - location: 1, - binding: 0, - format: VK_FORMAT_R32G32_SFLOAT, - offset: uint32(offsetOf(Vertex, uv)), - ), - VkVertexInputAttributeDescription( - location: 2, - binding: 0, - format: VK_FORMAT_R8G8B8A8_UNORM, - offset: uint32(offsetOf(Vertex, color)), - ), - VkVertexInputAttributeDescription( - location: 3, - binding: 0, - format: VK_FORMAT_R8G8B8A8_UNORM, - offset: uint32(offsetOf(Vertex, fillMidColor)), - ), - VkVertexInputAttributeDescription( - location: 4, - binding: 0, - format: VK_FORMAT_R8G8B8A8_UNORM, - offset: uint32(offsetOf(Vertex, fillStopColor)), - ), - VkVertexInputAttributeDescription( - location: 5, - binding: 0, - format: VK_FORMAT_R32G32B32A32_SFLOAT, - offset: uint32(offsetOf(Vertex, sdfParams)), - ), - VkVertexInputAttributeDescription( - location: 6, - binding: 0, - format: VK_FORMAT_R32G32B32A32_SFLOAT, - offset: uint32(offsetOf(Vertex, sdfRadii)), - ), - VkVertexInputAttributeDescription( - location: 7, - binding: 0, - format: VK_FORMAT_R16_UINT, - offset: uint32(offsetOf(Vertex, sdfMode)), - ), - VkVertexInputAttributeDescription( - location: 8, - binding: 0, - format: VK_FORMAT_R32G32_SFLOAT, - offset: uint32(offsetOf(Vertex, sdfFactors)), - ), - VkVertexInputAttributeDescription( - location: 9, - binding: 0, - format: VK_FORMAT_R32G32B32A32_SFLOAT, - offset: uint32(offsetOf(Vertex, rectMaskParams)), - ), - VkVertexInputAttributeDescription( - location: 10, - binding: 0, - format: VK_FORMAT_R32G32B32A32_SFLOAT, - offset: uint32(offsetOf(Vertex, rectMaskRadii)), - ), - VkVertexInputAttributeDescription( - location: 11, - binding: 0, - format: VK_FORMAT_R32G32B32A32_SFLOAT, - offset: uint32(offsetOf(Vertex, rectMaskMatX)), - ), - VkVertexInputAttributeDescription( - location: 12, - binding: 0, - format: VK_FORMAT_R32G32B32A32_SFLOAT, - offset: uint32(offsetOf(Vertex, rectMaskMatY)), - ), - ] + var attrDescs = + @[ + VkVertexInputAttributeDescription( + location: 0, + binding: 0, + format: VK_FORMAT_R32G32_SFLOAT, + offset: uint32(offsetOf(Vertex, pos)), + ), + VkVertexInputAttributeDescription( + location: 1, + binding: 0, + format: VK_FORMAT_R32G32_SFLOAT, + offset: uint32(offsetOf(Vertex, uv)), + ), + VkVertexInputAttributeDescription( + location: 2, + binding: 0, + format: VK_FORMAT_R8G8B8A8_UNORM, + offset: uint32(offsetOf(Vertex, color)), + ), + VkVertexInputAttributeDescription( + location: 3, + binding: 0, + format: VK_FORMAT_R8G8B8A8_UNORM, + offset: uint32(offsetOf(Vertex, fillMidColor)), + ), + VkVertexInputAttributeDescription( + location: 4, + binding: 0, + format: VK_FORMAT_R8G8B8A8_UNORM, + offset: uint32(offsetOf(Vertex, fillStopColor)), + ), + VkVertexInputAttributeDescription( + location: 5, + binding: 0, + format: VK_FORMAT_R32G32B32A32_SFLOAT, + offset: uint32(offsetOf(Vertex, sdfParams)), + ), + VkVertexInputAttributeDescription( + location: 6, + binding: 0, + format: VK_FORMAT_R32G32B32A32_SFLOAT, + offset: uint32(offsetOf(Vertex, sdfRadii)), + ), + VkVertexInputAttributeDescription( + location: 7, + binding: 0, + format: VK_FORMAT_R16_UINT, + offset: uint32(offsetOf(Vertex, sdfMode)), + ), + VkVertexInputAttributeDescription( + location: 8, + binding: 0, + format: VK_FORMAT_R32G32_SFLOAT, + offset: uint32(offsetOf(Vertex, sdfFactors)), + ), + VkVertexInputAttributeDescription( + location: 9, + binding: 0, + format: VK_FORMAT_R32G32B32A32_SFLOAT, + offset: uint32(offsetOf(Vertex, rectMaskParams)), + ), + VkVertexInputAttributeDescription( + location: 10, + binding: 0, + format: VK_FORMAT_R32G32B32A32_SFLOAT, + offset: uint32(offsetOf(Vertex, rectMaskRadii)), + ), + VkVertexInputAttributeDescription( + location: 11, + binding: 0, + format: VK_FORMAT_R32G32B32A32_SFLOAT, + offset: uint32(offsetOf(Vertex, rectMaskMatX)), + ), + VkVertexInputAttributeDescription( + location: 12, + binding: 0, + format: VK_FORMAT_R32G32B32A32_SFLOAT, + offset: uint32(offsetOf(Vertex, rectMaskMatY)), + ), + ] + when fastRectMaskLimit >= 2: + attrDescs.add( + VkVertexInputAttributeDescription( + location: 13, + binding: 0, + format: VK_FORMAT_R32G32B32A32_SFLOAT, + offset: uint32(offsetOf(Vertex, rectMaskParams2)), + ) + ) + attrDescs.add( + VkVertexInputAttributeDescription( + location: 14, + binding: 0, + format: VK_FORMAT_R32G32B32A32_SFLOAT, + offset: uint32(offsetOf(Vertex, rectMaskRadii2)), + ) + ) + attrDescs.add( + VkVertexInputAttributeDescription( + location: 15, + binding: 0, + format: VK_FORMAT_R32G32B32A32_SFLOAT, + offset: uint32(offsetOf(Vertex, rectMaskMatX2)), + ) + ) + attrDescs.add( + VkVertexInputAttributeDescription( + location: 16, + binding: 0, + format: VK_FORMAT_R32G32B32A32_SFLOAT, + offset: uint32(offsetOf(Vertex, rectMaskMatY2)), + ) + ) let vertexInputInfo = newVkPipelineVertexInputStateCreateInfo( vertexBindingDescriptions = [bindingDesc], vertexAttributeDescriptions = attrDescs @@ -2001,6 +2057,23 @@ proc flush(ctx: VulkanContext) = v.rectMaskMatY[1] = ctx.rectMaskMatY[i * 4 + 1] v.rectMaskMatY[2] = ctx.rectMaskMatY[i * 4 + 2] v.rectMaskMatY[3] = ctx.rectMaskMatY[i * 4 + 3] + when fastRectMaskLimit >= 2: + v.rectMaskParams2[0] = ctx.rectMaskParams2[i * 4 + 0] + v.rectMaskParams2[1] = ctx.rectMaskParams2[i * 4 + 1] + v.rectMaskParams2[2] = ctx.rectMaskParams2[i * 4 + 2] + v.rectMaskParams2[3] = ctx.rectMaskParams2[i * 4 + 3] + v.rectMaskRadii2[0] = ctx.rectMaskRadii2[i * 4 + 0] + v.rectMaskRadii2[1] = ctx.rectMaskRadii2[i * 4 + 1] + v.rectMaskRadii2[2] = ctx.rectMaskRadii2[i * 4 + 2] + v.rectMaskRadii2[3] = ctx.rectMaskRadii2[i * 4 + 3] + v.rectMaskMatX2[0] = ctx.rectMaskMatX2[i * 4 + 0] + v.rectMaskMatX2[1] = ctx.rectMaskMatX2[i * 4 + 1] + v.rectMaskMatX2[2] = ctx.rectMaskMatX2[i * 4 + 2] + v.rectMaskMatX2[3] = ctx.rectMaskMatX2[i * 4 + 3] + v.rectMaskMatY2[0] = ctx.rectMaskMatY2[i * 4 + 0] + v.rectMaskMatY2[1] = ctx.rectMaskMatY2[i * 4 + 1] + v.rectMaskMatY2[2] = ctx.rectMaskMatY2[i * 4 + 2] + v.rectMaskMatY2[3] = ctx.rectMaskMatY2[i * 4 + 3] let uploadBytes = VkDeviceSize(vertexCount * sizeof(Vertex)) let vertexAlloc = ctx.createBuffer( @@ -2175,6 +2248,16 @@ proc setRectMaskVert4( ctx.rectMaskMatX.setVert4(offset + i, matX) ctx.rectMaskMatY.setVert4(offset + i, matY) +when fastRectMaskLimit >= 2: + proc setRectMask2Vert4( + ctx: VulkanContext, offset: int, params, radii, matX, matY: Vec4 + ) = + for i in 0 ..< 4: + ctx.rectMaskParams2.setVert4(offset + i, params) + ctx.rectMaskRadii2.setVert4(offset + i, radii) + ctx.rectMaskMatX2.setVert4(offset + i, matX) + ctx.rectMaskMatY2.setVert4(offset + i, matY) + proc setDisabledRectMaskVerts(ctx: VulkanContext, firstVertex, vertexCount: int) = let params = vec4(0.0'f32, 0.0'f32, -1.0'f32, -1.0'f32) @@ -2184,40 +2267,77 @@ proc setDisabledRectMaskVerts(ctx: VulkanContext, firstVertex, vertexCount: int) ctx.rectMaskRadii.setVert4(i, zero4) ctx.rectMaskMatX.setVert4(i, zero4) ctx.rectMaskMatY.setVert4(i, zero4) + when fastRectMaskLimit >= 2: + ctx.rectMaskParams2.setVert4(i, params) + ctx.rectMaskRadii2.setVert4(i, zero4) + ctx.rectMaskMatX2.setVert4(i, zero4) + ctx.rectMaskMatY2.setVert4(i, zero4) + +proc fastRectMaskCount(ctx: VulkanContext): int = + for rectMask in ctx.rectMaskStack: + if rectMask.kind == rmkFast: + inc result proc setRectMaskVert4(ctx: VulkanContext, offset: int) = - if ctx.maskBegun: + when fastRectMaskLimit == 0: return + else: + if ctx.maskBegun: + return - var - hasRectMask = false - params = vec4(0.0'f32) - radii = vec4(0.0'f32) - matX = vec4(0.0'f32) - matY = vec4(0.0'f32) - - if ctx.rectMaskStack.len > 0: - for i in countdown(ctx.rectMaskStack.len - 1, 0): - let rectMask = ctx.rectMaskStack[i] - if rectMask.kind == rmkFast: - hasRectMask = true - params = rectMask.params - radii = rectMask.radii - matX = rectMask.matX - matY = rectMask.matY - break - - if hasRectMask: - if not ctx.batchHasRectMask: - ctx.setDisabledRectMaskVerts(0, offset) - ctx.batchHasRectMask = true - ctx.setRectMaskVert4(offset, params, radii, matX, matY) - elif ctx.batchHasRectMask: - ctx.setDisabledRectMaskVerts(offset, 4) + var + maskCount = 0 + params = vec4(0.0'f32) + radii = vec4(0.0'f32) + matX = vec4(0.0'f32) + matY = vec4(0.0'f32) + when fastRectMaskLimit >= 2: + var + params2 = vec4(0.0'f32) + radii2 = vec4(0.0'f32) + matX2 = vec4(0.0'f32) + matY2 = vec4(0.0'f32) + + if ctx.rectMaskStack.len > 0: + for i in countdown(ctx.rectMaskStack.len - 1, 0): + let rectMask = ctx.rectMaskStack[i] + if rectMask.kind == rmkFast: + if maskCount == 0: + params = rectMask.params + radii = rectMask.radii + matX = rectMask.matX + matY = rectMask.matY + inc maskCount + elif fastRectMaskLimit >= 2 and maskCount == 1: + when fastRectMaskLimit >= 2: + params2 = rectMask.params + radii2 = rectMask.radii + matX2 = rectMask.matX + matY2 = rectMask.matY + inc maskCount + if maskCount >= fastRectMaskLimit: + break + + if maskCount > 0: + if not ctx.batchHasRectMask: + ctx.setDisabledRectMaskVerts(0, offset) + ctx.batchHasRectMask = true + ctx.setRectMaskVert4(offset, params, radii, matX, matY) + when fastRectMaskLimit >= 2: + if maskCount >= 2: + ctx.setRectMask2Vert4(offset, params2, radii2, matX2, matY2) + else: + let + disabledParams = vec4(0.0'f32, 0.0'f32, -1.0'f32, -1.0'f32) + zero4 = vec4(0.0'f32) + ctx.setRectMask2Vert4(offset, disabledParams, zero4, zero4, zero4) + elif ctx.batchHasRectMask: + ctx.setDisabledRectMaskVerts(offset, 4) template setRectMaskVert4IfNeeded(ctx: VulkanContext, offset: int) = - if not ctx.maskBegun and (ctx.batchHasRectMask or ctx.rectMaskStack.len > 0): - ctx.setRectMaskVert4(offset) + when fastRectMaskLimit > 0: + if not ctx.maskBegun and (ctx.batchHasRectMask or ctx.rectMaskStack.len > 0): + ctx.setRectMaskVert4(offset) proc copyIntoAtlas(atlas: Image, atX, atY: int, image: Image) = for y in 0 ..< image.height: @@ -3282,7 +3402,8 @@ method beginRectMask*( assert ctx.frameBegun == true, "ctx.beginFrame has not been called." assert ctx.maskBegun == false, "ctx.beginRectMask cannot start inside a mask." - if ctx.rectMaskStack.len == 0 and maskRect.w > 0.0'f32 and maskRect.h > 0.0'f32: + if fastRectMaskLimit > 0 and ctx.fastRectMaskCount() < fastRectMaskLimit and + maskRect.w > 0.0'f32 and maskRect.h > 0.0'f32: ctx.rectMaskStack.add(ctx.makeRectMask(maskRect, radii)) else: ctx.beginMask(maskRect, radii) @@ -3637,6 +3758,11 @@ proc newContext*( result.rectMaskRadii = newSeq[float32](4 * maxQuads * 4) result.rectMaskMatX = newSeq[float32](4 * maxQuads * 4) result.rectMaskMatY = newSeq[float32](4 * maxQuads * 4) + when fastRectMaskLimit >= 2: + result.rectMaskParams2 = newSeq[float32](4 * maxQuads * 4) + result.rectMaskRadii2 = newSeq[float32](4 * maxQuads * 4) + result.rectMaskMatX2 = newSeq[float32](4 * maxQuads * 4) + result.rectMaskMatY2 = newSeq[float32](4 * maxQuads * 4) result.vertexScratch = newSeq[Vertex](maxQuads * 4) result.indices = newSeq[uint16](maxQuads * 6)