feat(draw): a POLY op, so rotated solid geometry carries coverage - #301
Open
qianiaoo wants to merge 1 commit into
Open
feat(draw): a POLY op, so rotated solid geometry carries coverage#301qianiaoo wants to merge 1 commit into
qianiaoo wants to merge 1 commit into
Conversation
`TRI` has no coverage field, so a rotated solid box resolves to two grey levels at any resolution — `emit_box` -> Sutherland-Hodgman -> `emit_tri` rounds every vertex to an integer pixel and there is nowhere to put a partial one. Recorded at `draw.rs:10-18` as a v1 degradation; what was missing was the price. `POLY` (opcode 10, `3 + N` words) carries the whole clipped convex polygon and one flat colour, so coverage is computed over the shape rather than per triangle. Per-triangle coverage is the wrong fix and the guard says so: two sequential blends are not one blend, and the shared diagonal of a rotated box keeps 68 interior partial pixels. Over the polygon it keeps none. It is not slower. `poly()` solves each scanline for the fully-interior x-range, fills it as one run and samples 4x4 only at the ends — O(perimeter), not O(area) — against a `tri` that evaluated three `orient()` calls for every pixel of the bounding box with no incremental stepping. Measured at 0.99x on a standalone bench and 22% faster end to end on eight rotated bars at 1080p. The inner loop stays integer: edge functions in 4*F fixed point, quarter-pixel offsets as +/-1 and +/-3, `div_euclid` for the span solve. No float enters it, so the frame-hash contract carries over. Hardware backends without per-pixel coverage decode `POLY` to a triangle fan — today's binary fill, byte-identical output. `Fill::Grad` keeps its TRI fan. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
qianiaoo
force-pushed
the
feat/poly-op-coverage
branch
from
August 19, 2026 07:14
22e3d98 to
894603b
Compare
qianiaoo
marked this pull request as ready for review
August 19, 2026 10:05
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rotated solid geometry has binary edges today, and the reason is in the op set
rather than in any rasterizer:
TRIhas no coverage field.draw.rscomputes fractional coverage for axis-aligned content only —scale_alpha_coverage/pixel_interval_coverageon RECT spans, roundedcorners and shadows. A rotated box takes the other path,
emit_box→Sutherland-Hodgman →
emit_tri, which rounds every vertex to an integerpixel and emits 7 words: opcode, three positions, three vertex colours. There
is nowhere to put a partial pixel. A white box at 20° on black therefore
resolves to exactly 2 grey levels, at any resolution — 4K makes the steps
smaller without making them fewer. This is recorded at
draw.rs:10-18as a v1degradation, so it is a known limitation, not a regression; what was missing was
the price of removing it.
This adds
POLY(opcode 10,3 + Nwords: op, vertex count N in 3..=8,one packed flat colour, N ×
xy_word).emit_boxemits it forFill::Flatand the
Item3::Quad3D-face path emits it after projection; both afterSutherland-Hodgman, so the op is already clipped and convex.
Per-triangle coverage is the wrong fix, and the test says so
The obvious cheaper change — keep
TRI, give it an alpha — does not work. Arotated rectangle is two triangles sharing a diagonal, and two sequential alpha
blends are not one blend: 0.5 over 0.5 leaves 0.75. Same box (240×160 at 20°),
counting pixels that are neither 0 nor 255 while all eight neighbours are
non-background:
Those 68 are a seam down the shared diagonal of every rotated box, and the 21
levels are that artefact rather than an improvement — 16 samples can only
produce 17, and the extra four are double-blended pixels. Coverage has to be
computed over the whole clipped polygon, which is why this needs an op and not
a field.
rotated_flat_box_has_no_interior_partial_pixelsis that regression as aguard: fan the polygon back into
emit_polycalls per triangle and it fails atexactly 68.
It is not slower
poly()solves each scanline for the fully-interior x-range, fills that as onerun, and samples 4×4 only at the two ends. Boundary work is O(perimeter), not
O(area). The op it replaces evaluates three
orient()calls — six i64multiplies — for every pixel in the bounding box, with no incremental
stepping and no run fill, so the span solve buys back more than the coverage
test costs.
Standalone benchmark, inner loops lifted from
raster.rsso they match, fiverepetitions, 1920×1080, eight 120×800 bars at 4°–11°, Apple M4 native
(
opt-level=3 lto=true codegen-units=1):End to end through the wasm host, whole frames including tick and draw:
Only ratios taken inside one process are quoted: absolute times on this machine
drift up to 1.8× between processes.
For scale, 4× MSAA — the games-industry default — resolves each edge to 5
levels. 17 is what 4×4 sampling can produce, and it is the level that comes out
free.
Determinism
The inner loop is integer throughout: edge functions in 4·F fixed point so
the quarter-pixel sample offsets stay integral as ±1/±3,
div_euclidfor thespan solve, winding taken from the doubled-coordinate shoelace area. No float
enters it, so the frame-hash contract carries over unchanged — which is the
constraint that made 4×4 the design rather than an analytic area.
What each backend does
raster.rs) — the coverage implementation.esp32p4-ppadelegates through
software_op, so one change covers both.so
POLYdecodes to a triangle fan: today's binary fill of the same convexpolygon, byte-identical output, no regression and no benefit.
POLYs stay vector paths alongside flatTRIs; a batchcontaining any gouraud or textured member still goes through the core
rasterizer whole, so painter order inside a depth-sorted 3D subtree is
preserved.
POLYis flat-coloured by construction.damage.rs— stride and bounds, with the same 3..=8 validation.Decoders return or break on a malformed
POLYrather than guessing, matchinghow the closed op set is handled elsewhere.
Scope
Fill::Gradkeeps its TRI fan. Gradient corners interpolate per vertex andthe fan is where that happens; rotated gradient boxes still have binary edges.
rotated_flat_box_emits_one_poly_gradient_stays_tripins the split.yields at most 8 vertices, so the bound is the geometry's, not a budget.
TEX_TRIis untouched — textured meshes still subdivide.Who this helps
Worth stating, because the answer is not "everyone".
The software rasterizer, and gpui. Those are the two backends that can
honour per-pixel coverage;
esp32p4-ppainherits it by delegating to the samerasterizer.
Not PSP, Vita or Symbian. Their hardware has no per-pixel coverage, so
POLYdecodes to a triangle fan and their output is byte-identical to today.They pay a decoder case and get nothing back. That is the trade this PR asks
for, and it should be weighed rather than discovered.
In this repository the demand is one app.
rotate-Nappears seven times inthe whole tree, all of them in
apps/motions, and every one on a roundedbox —
rounded-[999px]pills atrotate-28/rotate-332insiderotate-140and
rotate-320containers, and arounded-[5px]card atrotate-8.Rounded is not a separate path:
draw.rs:1999sends any non-axis-alignedrounded box to
emit_box, dropping the radius. All seven therefore reachPOLY, and all seven have binary edges today.apps/motionsis also the app#296 identified as the first whose DrawList exercises the rotated/3D
raster-fallback path.
Seven usages in one demo app is thin evidence of demand, and it is why this is
a draft rather than a claim that the op is overdue. If the project's centre of
gravity is still the fixed-function hosts, this is 800 lines of permanent
decoder tax for an op most of them cannot honour, and closing it is a
reasonable call.
Verified
bun run test: 11/11 stages green, including the compiler smoke buildsand the launcher sim.
engine/core: 126 tests pass, five of them new —rotated_flat_box_emits_one_poly_gradient_stays_tri,rotated_flat_box_raster_has_coverage_levels,rotated_flat_box_has_no_interior_partial_pixels,clipped_polygon_closes_against_the_screen_edge,rotated_3d_face_emits_poly_textured_still_tex_tri.polygon into per-triangle
emit_polycalls fails it at exactly 68, thesame 68 the standalone benchmark counts.
engine/core/src/spec.rsregenerates identically underbun run gen.cargo checkclean onpocket-ui-wgpu,esp32p4-ppaandengine/wasm.engine/backends/gpuidoes notbuild on the machine this was written on — nor does it on clean
main, thegpui 0.2.2build script fails Metal shader compilation there — so the gpuidecoder was pushed unverified by compilation.
macOS gpui backendnow passes,which is that verification. It first went red on
clippy::manual_range_contains: six decoders wroten < 3 || n > 8wheredamage.rswrote!(3..=8).contains(&n), and upstream lints with-D warnings. All six now read the idiomatic form. Clippy is not installedunder rustup on that machine and only Homebrew's is, built against a different
rustc, so the lint is verified here rather than locally.
decoders emit a fan;
ESP-IDF release/v6.0andRust rendererpass.Found while driving the DrawList from a headless deterministic video renderer,
where rotated bars over a dark field make the missing coverage impossible to
miss.