git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commite42e757f698e4754ec59dc8aba27988b8c1ae87a
parent56e0f713aa
authorLucas Galante <[email protected]>
date2026-07-31 12:51
fix: wireframe pass draws both facet sets — culling is winding-unreliable in LINE mode

Pure wireframe showed ONLY the far hemisphere's interior (near cap
absent): with the negative-height viewport, at least some drivers skip
the framebuffer-space Y-mirror when evaluating winding for LINE polygon
mode, so the wire pipeline culled the OPPOSITE facet set from the fill.
That also reframes the old overlay saga: the -4/-1 depth bias was
punching those far-side wires through the near fill — the overlay's
whole lattice was the back side showing through.

The wire pipeline now culls NOTHING (facet selection by depth, not
winding): pure wireframe is the full cage on every driver, and in
overlay mode the near fill's depth hides the far side. Bias resized to
the actual tiebreak need — line fragments sample ≤ half a pixel off the
fill's centers, so slope -0.5 + constant -1; a faint far-wire bleed
survives in the thin limb band where near/far depths converge.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/vk/scene.rs | 38 ++++++++++++++++++++++++--------------
 1 file changed, 24 insertions(+), 14 deletions(-)

diff --git a/src/vk/scene.rs b/src/vk/scene.rs
index 42b8700..542616b 100644
--- a/src/vk/scene.rs
+++ b/src/vk/scene.rs
@@ -287,17 +287,27 @@ impl SceneStage {
                 )
                 .expect("Failed to create 3D pipeline")[0];
 
-            // The wireframe twin: identical but rasterized as lines. Culling
-            // stays on so the wire view matches the fill's visible surface.
-            // A small negative depth bias pulls the lines toward the viewer,
-            // and the compare is LESS_OR_EQUAL with writes off: a line
-            // fragment over its own fill interpolates the SAME plane, and on
-            // camera-facing facets (near-zero depth slope) the biased
-            // difference dips below depth precision — under strict LESS the
-            // front-center lattice z-fought the coplanar fill and LOST, wires
-            // surviving only toward the limb, which read as the mesh
-            // counter-rotating during orbits (the overlay-mode "sphere turns
-            // with the camera" illusion).
+            // The wireframe twin: identical but rasterized as lines, and
+            // culling OFF. Culling cannot be trusted here: winding is
+            // evaluated in framebuffer space, and with the negative-height
+            // viewport at least some drivers skip the Y-mirror for LINE
+            // polygon mode — the wire pass then selects the OPPOSITE facet
+            // set from the fill (observed live: pure-wireframe spheres drew
+            // only the far hemisphere's interior, near cap absent). With cull
+            // NONE the wire view is driver-independent: pure wireframe is the
+            // full cage, and in overlay mode the DEPTH test — not culling —
+            // hides the far side (far wires fail LESS_OR_EQUAL against the
+            // near fill).
+            // The compare is LESS_OR_EQUAL with writes off, plus a bias
+            // tiebreaker sized to the actual error source: a line fragment
+            // samples up to ~half a pixel off the fill's pixel centers, so
+            // its depth misses the fill's by ≤ 0.5 × the facet's depth slope
+            // — slope factor -0.5 covers exactly that, and -1 constant
+            // covers rounding. NO MORE: the old -4/-1 was strong enough to
+            // punch FAR-side wires through the near fill (with the culled-
+            // facet flip above, those far wires were the only wires — the
+            // overlay's whole lattice was the back side showing through,
+            // which read as the mesh counter-rotating during orbits).
             let depth_stencil_lines = vk::PipelineDepthStencilStateCreateInfo::default()
                 .depth_test_enable(true)
                 .depth_write_enable(false)
@@ -305,11 +315,11 @@ impl SceneStage {
             let wireframe_pipeline = wireframe_supported.then(|| {
                 let rasterization_lines = vk::PipelineRasterizationStateCreateInfo::default()
                     .polygon_mode(vk::PolygonMode::LINE)
-                    .cull_mode(vk::CullModeFlags::BACK)
+                    .cull_mode(vk::CullModeFlags::NONE)
                     .front_face(vk::FrontFace::COUNTER_CLOCKWISE)
                     .depth_bias_enable(true)
-                    .depth_bias_constant_factor(-4.0)
-                    .depth_bias_slope_factor(-1.0)
+                    .depth_bias_constant_factor(-1.0)
+                    .depth_bias_slope_factor(-0.5)
                     .line_width(1.0);
                 device
                     .create_graphics_pipelines(