diff --git a/ss_player/ss_internal_player.cpp b/ss_player/ss_internal_player.cpp index 2ec044c..8796a75 100644 --- a/ss_player/ss_internal_player.cpp +++ b/ss_player/ss_internal_player.cpp @@ -978,6 +978,18 @@ bool SsInternalPlayer::_build_mask_writers(const DrawFrame& f) { // drawing its own colour over the scene. if (pure_mask) pure_mask_flags[p_idx] = 1; + // A writer only writes where its per-frame mask is non-zero and it is not + // hidden (Rule_Mask.md ยง2-2). Both live in FrameData, so this is the one + // half of the classification PartData cannot answer: an authored MASK of 0 + // is how a writer is switched off over a range of frames, and the cutout + // threshold cannot stand in for it โ€” a shape mask samples no texture, so + // its coverage is its whole geometry whatever the threshold says. A part + // with no PartState this frame was not evaluated and writes nothing, which + // is what `_bake_coverage_geometry` would decide about it anyway. + // Tested before the bitmap budget so a switched-off writer costs no bit. + const auto* ps = (p_idx < (int)_parts_by_idx.size()) ? _parts_by_idx[p_idx] : nullptr; + if (!ps || ps->mask() == 0.0f || ps->hide()) continue; + // Bitmap holds 24 writers; keep scanning past that so the pure masks that // did not fit are still flagged above. if ((int)_mask_writers.size() >= MAX_MASK_WRITERS) continue; diff --git a/ss_player/ss_internal_player.h b/ss_player/ss_internal_player.h index 83d6f97..d5349b5 100644 --- a/ss_player/ss_internal_player.h +++ b/ss_player/ss_internal_player.h @@ -555,7 +555,8 @@ class SsInternalPlayer { // ---- CBP masking (clever bit packing) ---------------------------------- // One entry per part that writes the mask this frame. Rebuilt by - // `_build_mask_writers` each frame from draw_order + static PartData. + // `_build_mask_writers` each frame from draw_order, static PartData, and the + // per-frame mask / hide that say whether a writer writes at all. // `bit` is the writer's slot in the coverage bitmap (0..MAX_MASK_WRITERS-1). // `op_invert` comes from PartData.mask_influence (false=increment / true= // invert). `is_clipping` selects the scope direction: a pure Mask part @@ -575,11 +576,13 @@ class SsInternalPlayer { Vector _mask_writers; // Per part index (parallel to `_parts_by_idx`): 1 when the part is a "pure" // mask this frame. Rebuilt with `_mask_writers` so the emit paths can test it - // once per part without walking the writer list. + // once per part without walking the writer list. A pure mask keeps this flag + // when it writes nothing โ€” switched off, hidden, or past the bit budget โ€” so + // that it still draws no colour of its own. LocalVector _part_pure_mask; - // Populate `_mask_writers` from this frame's draw_order + static PartData. - // Returns true if at least one writer is present (i.e., masking is active - // this frame). Only the top-root player owns the mask state. + // Populate `_mask_writers` from this frame's draw_order, static PartData and + // per-frame mask / hide. Returns true if at least one writer is present (i.e., + // masking is active this frame). Only the top-root player owns the mask state. bool _build_mask_writers(const DrawFrame& f); // ---- CBP coverage bitmap (offscreen RGBA8 = 32 mask bits) --------------