diff --git a/Plugins/ZShell/Blobs/CMakeLists.txt b/Plugins/ZShell/Blobs/CMakeLists.txt index bf12181..05ec0d9 100644 --- a/Plugins/ZShell/Blobs/CMakeLists.txt +++ b/Plugins/ZShell/Blobs/CMakeLists.txt @@ -12,6 +12,7 @@ qml_module(ZShell-blobs qt_add_shaders(ZShell-blobs "blob_shaders" BATCHABLE OPTIMIZED NOHLSL NOMSL + GLSL "300es,330" PREFIX "/" FILES shaders/blob.frag diff --git a/Plugins/ZShell/Blobs/blobmaterial.cpp b/Plugins/ZShell/Blobs/blobmaterial.cpp index 688315a..8bb85b3 100644 --- a/Plugins/ZShell/Blobs/blobmaterial.cpp +++ b/Plugins/ZShell/Blobs/blobmaterial.cpp @@ -2,6 +2,9 @@ #include +static_assert(sizeof(decltype(BlobRectData::excludeMask)) == sizeof(float), + "BlobMaterial packs excludeMask into a float slot via memcpy"); + QSGMaterialType* BlobMaterial::type() const { static QSGMaterialType s_type; return &s_type; @@ -82,8 +85,11 @@ bool BlobMaterialShader::updateUniformData(RenderState& state, QSGMaterial* newM for (int i = 0; i < count; ++i) { const auto& r = mat->m_rects[i]; const int base = 160 + i * 80; + // Pack excludeMask into props.x via bit-cast (read in shader with floatBitsToInt) + float maskAsFloat; + memcpy(&maskAsFloat, &r.excludeMask, sizeof(float)); const float d0[4] = { r.cx, r.cy, r.hw, r.hh }; - const float d1[4] = { 0.0f, r.offsetX, r.offsetY, r.minEig }; + const float d1[4] = { maskAsFloat, r.offsetX, r.offsetY, r.minEig }; const float d3[4] = { r.screenHalfX, r.screenHalfY, 0.0f, 0.0f }; memcpy(buf->data() + base, d0, 16); memcpy(buf->data() + base + 16, d1, 16); diff --git a/Plugins/ZShell/Blobs/blobmaterial.hpp b/Plugins/ZShell/Blobs/blobmaterial.hpp index 9e3518b..0a11f10 100644 --- a/Plugins/ZShell/Blobs/blobmaterial.hpp +++ b/Plugins/ZShell/Blobs/blobmaterial.hpp @@ -14,6 +14,9 @@ struct BlobRectData { float screenHalfX = 0, screenHalfY = 0; // Effective per-corner radii (tr, br, bl, tl), pre-computed on CPU float radius[4] = { 0, 0, 0, 0 }; + // Bitmask of indices in this rect's m_cachedRects that mutually exclude (or are excluded by) this rect. + // Used by the shader to skip smin between excluded pairs. + int excludeMask = 0; }; class BlobMaterial : public QSGMaterial { diff --git a/Plugins/ZShell/Blobs/blobshape.cpp b/Plugins/ZShell/Blobs/blobshape.cpp index ce05808..85bebf2 100644 --- a/Plugins/ZShell/Blobs/blobshape.cpp +++ b/Plugins/ZShell/Blobs/blobshape.cpp @@ -149,6 +149,10 @@ void BlobShape::updatePolish() { const QRectF myPadded(static_cast(m_cachedPaddedX), static_cast(m_cachedPaddedY), static_cast(m_cachedPaddedW), static_cast(m_cachedPaddedH)); + // Track shape pointers parallel to m_cachedRects for pairwise exclusion lookups + QVector rectShapes; + rectShapes.reserve(m_group->shapes().size()); + for (BlobShape* other : m_group->shapes()) { if (other->isInvertedRect()) continue; @@ -210,12 +214,29 @@ void BlobShape::updatePolish() { r.screenHalfY = std::abs(b) * r.hw + std::abs(d) * r.hh; m_cachedRects.append(r); + rectShapes.append(other); } } if (isInvertedRect()) m_cachedMyIndex = -1; + // Compute pairwise exclude masks. Bit j in entry i is set iff rect i excludes rect j + // or rect j excludes rect i. The shader uses this to avoid smin between excluded pairs. + const auto cachedCount = m_cachedRects.size(); + for (qsizetype i = 0; i < cachedCount; ++i) { + int mask = 0; + BlobShape* si = rectShapes[i]; + for (qsizetype j = 0; j < cachedCount; ++j) { + if (j == i) + continue; + BlobShape* sj = rectShapes[j]; + if (si->isExcluded(sj) || sj->isExcluded(si)) + mask |= (1 << j); + } + m_cachedRects[i].excludeMask = mask; + } + // Cache inverted rect data m_cachedHasInverted = false; m_cachedInvertedRadius = 0; @@ -270,6 +291,7 @@ void BlobShape::updatePolish() { const auto rectCount = m_cachedRects.size(); for (qsizetype i = 0; i < rectCount; ++i) { auto& ri = m_cachedRects[i]; + const int riExcludeMask = ri.excludeMask; float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f; const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh; @@ -280,6 +302,8 @@ void BlobShape::updatePolish() { for (qsizetype j = 0; j < rectCount; ++j) { if (j == i) continue; + if (riExcludeMask & (1 << j)) + continue; const auto& rj = m_cachedRects[j]; fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh))); fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh))); diff --git a/Plugins/ZShell/Blobs/shaders/blob.frag b/Plugins/ZShell/Blobs/shaders/blob.frag index 4f3bd45..ddee3f7 100644 --- a/Plugins/ZShell/Blobs/shaders/blob.frag +++ b/Plugins/ZShell/Blobs/shaders/blob.frag @@ -63,13 +63,17 @@ float smaxSharpA(float a, float b, float k) { void main() { vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH); - float mergedSdf = 1e10; + // Phase 1: compute per-rect SDF, track owner. We can't smin yet because + // excluded pairs need to skip the smooth blend, which requires pairwise pass + // below. + float dArr[16]; int owner = -2; float minDist = 1e10; for (int i = 0; i < rectCount; i++) { - vec4 rect = rectData[i * 5]; // cx, cy, hw, hh - vec4 props = rectData[i * 5 + 1]; // radius, offsetX, offsetY, minEig + vec4 rect = rectData[i * 5]; // cx, cy, hw, hh + vec4 props = + rectData[i * 5 + 1]; // excludeMask(int bits), offsetX, offsetY, minEig vec4 invDm = rectData[i * 5 + 2]; // inverse deform matrix vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, 0, 0 vec4 radii = @@ -81,8 +85,10 @@ void main() { // AABB early-out: skip rects far from this pixel vec2 extent = sh.xy + vec2(smoothFactor * 1.5); if (abs(pixel.x - center.x) > extent.x || - abs(pixel.y - center.y) > extent.y) + abs(pixel.y - center.y) > extent.y) { + dArr[i] = 1e10; continue; + } // Apply pre-computed inverse deformation to the evaluation point mat2 invDeform = mat2(invDm.xy, invDm.zw); @@ -138,13 +144,38 @@ void main() { d *= scale; } - mergedSdf = smin(mergedSdf, d, smoothFactor); + dArr[i] = d; if (d < smoothFactor && d < minDist) { minDist = d; owner = i; } } + // Phase 2: hard-min baseline over all rects. + float mergedSdf = 1e10; + for (int i = 0; i < rectCount; i++) { + mergedSdf = min(mergedSdf, dArr[i]); + } + + // Phase 3: pair-wise smin contributions, skipping excluded pairs. Pair smin + // <= min, so taking the min over all non-excluded pair smins gives the + // smoothly-merged SDF. + for (int i = 0; i < rectCount; i++) { + if (dArr[i] >= 1e9) + continue; + int excludeMask = floatBitsToInt(rectData[i * 5 + 1].x); + for (int j = i + 1; j < rectCount; j++) { + if (dArr[j] >= 1e9) + continue; + if ((excludeMask & (1 << j)) != 0) + continue; + // smin only deviates from min within smoothFactor + if (abs(dArr[i] - dArr[j]) >= smoothFactor) + continue; + mergedSdf = min(mergedSdf, smin(dArr[i], dArr[j], smoothFactor)); + } + } + if (hasInverted != 0) { float dOuter = sdBox(pixel, invertedOuter.xy, invertedOuter.zw) - 1.0; float dInner =