fix(player): a mask writer switched off with MASK=0 writes nothing - #305
Merged
Merged
Conversation
`_build_mask_writers` decided what writes the coverage bitmap from static PartData alone, so a writer stayed active on every frame of every animation. Rule_Mask.md §2-2 puts half of that decision in FrameData: a writer writes only where its per-frame `mask` is non-zero and it is not hidden, and both checks belong to the Player. `mask` did reach the coverage shader as the cutout threshold, where `mask == 0` maps to 1.0 and discards every texel — but the shape branch overrides the threshold to -1.0, because a shape mask samples no texture and its coverage is its whole geometry. So the one writer kind that has no other way to be switched off was exactly the kind whose switch was bypassed. An authored `MASK` of 0 on a shape mask covered the screen instead of covering nothing, and every `visibleInsideMask` target passed everywhere. The test goes after the pure-mask flag, so a writer that writes nothing still draws no colour of its own, and before the 24-bit budget, so it costs no bit. Checked against a converted Spine clipping (19 pieces keyed 255 plus the switched-off rectangle that stands in for "not clipping"): the pieces stay writers and the rectangle no longer does. The nine writers in `tests/overall/Mask.ssae` are all keyed 255 and are unaffected, which is also why the suite never caught this.
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.
What was wrong
_build_mask_writersdecided what writes the coverage bitmap from static PartData alone (draw_as_mask/mask_write), so a writer was active on every frame of every animation.Rule_Mask.md§2-2 puts half of that decision in FrameData:and it says explicitly that both checks belong to the Player.
maskdid reach the coverage shader, as the cutout threshold —ss_mask_write.gdshaderdocuments the intent: "mask == 0 maps to threshold 1.0 so nothing is written (empty mask)". But the shape branch overrides the threshold to-1.0(no_cutout), correctly, because a shape mask samples no texture and its coverage is its whole geometry whatever the alpha says. So the one writer kind that has no other way to be switched off was exactly the kind whose switch was bypassed.Authoring a
MASKof 0 on a shape mask therefore covered the screen instead of covering nothing, and everyvisibleInsideMasktarget passed everywhere.Found on a Spine clipping converted to shape masks: the polygon becomes N pieces keyed
MASK=255plus one rectangle covering the canvas that is keyedMASK=0and stands in for "not clipping on this frame". That rectangle wrote coverage over everything, so the clipped sprite was visible on every frame instead of only inside the shape.The change
One test in
_build_mask_writers:Both placements matter:
pure_mask_flags[p_idx] = 1— a writer that writes nothing must still draw no colour of its own, the same reasoning theMAX_MASK_WRITERSoverflow already relies on.A part with no
PartStatethis frame was not evaluated and writes nothing, which is what_bake_coverage_geometrydecides about it anyway._bubble_child_clip_writersand_has_visible_clip_bubblingboth read the child's_mask_writers, so the single test covers the instance carry-over path too. Header comments that said the classification comes from static PartData are updated.Verification
scripts/build-extension.sh— clean.scripts/run-tests.sh—35 cases, 101 assertions, 0 failed, 0 skipped.mask=255) stay writers, the stand-in rectangle (mask=0) is now skipped. Before the change it was a writer covering ~9 canvases.tests/overall/Mask.ssaeare keyed 255 (one 254) and all stay writers. No writer anywhere in that project is keyed 0, which is why the suite never caught this — worth a case if the fixture ever grows one.Not in this PR
SpriteStudioMaskPlanner.BuildWriters(its doc comment saysmirrors _build_mask_writers) with the matchingDrawKind == Shape ? -1fthreshold override._resolve_part_maskreturns an unmasked decision, so avisibleInsideMasktarget draws unclipped where the Editor would drop it. Pre-existing, unchanged here.