Skip to content

fix(smx): gif pack option display and CanBeEmpty fallback semantics#616

Open
fchorney wants to merge 2 commits into
pnn64:mainfrom
fchorney:fc/smx-pack-row-display
Open

fix(smx): gif pack option display and CanBeEmpty fallback semantics#616
fchorney wants to merge 2 commits into
pnn64:mainfrom
fchorney:fc/smx-pack-row-display

Conversation

@fchorney

@fchorney fchorney commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Rebased onto current main. #617 is stacked on this branch, so this one lands first.

Two fixes for the gif-pack bug reports against v0.4.2004.

1. Selected gif pack shows as Default when re-entering the options screen

Selecting a pad or judgement gif pack on the StepManiaX options page appears not to stick: re-entering the options screen always shows Default checked, regardless of what was selected.

The saved value is actually fine the whole time: the config, the ini round-trip, and the pad lighting all keep the chosen pack. Only the displayed row selection resets.

Cause: the SmxBgPack / SmxJudgePack rows are defined with a single static placeholder choice; their real choice list (Default plus the discovered packs) is provided dynamically by the options layout. The screen-init sync used set_choice_by_id, which clamps the incoming index against the static row definition's choice list:

// src/screens/options/row.rs
let max_idx = rows[pos].choices.len().saturating_sub(1); // placeholder len = 1 -> max_idx = 0
*slot = idx.min(max_idx);                                // always clamped to 0 = "Default"

Fix: write the index directly via get_choice_by_id_mut, the same pattern already used by the other dynamically-populated rows (DefaultNoteSkin, SoftwareRendererThreads). The remaining dynamic rows (display mode/resolution/refresh rate, sound device) already use direct writes; the per-player pack rows in Player Options bake their full choice list into the row and are not affected.

2. CanBeEmpty roles still slid over to the pack's default gif

A pack that supplies all of its own gifs except gameplay and declares CanBeEmpty = "gameplay" expects no background during gameplay. The registry honored the declaration for the pack-level chain (no Fallback pack, no common), but the app's screen resolution then fell back to the default role, so the pack's own default gif showed during gameplay anyway.

Fix: the role-candidate walk (grade-specific results roles, then the screen role, then default) now stops dead at any candidate the selected pack declares under CanBeEmpty without supplying: that event shows no animation at all. CanBeEmpty now means what it says: this name never has an animation, no matter which fallback would have provided one.

Details:

  • A gif the pack supplies still wins over its own declaration (unchanged).
  • Per-song / per-pack scoped gifs sit above pack policy and are unaffected (they are an override, not a fallback).
  • New registry API GifRegistry::background_declared_empty exposes the declared-empty state (the existing background() lookup cannot distinguish "empty by declaration" from "missing, keep falling back"), with a unit test.
  • docs/stepmaniax.md updated: the resolution chain, the grade-chain section, the CanBeEmpty table row, and the "when to use" example now describe the stop-dead semantics. Also corrected the section that claimed an opted-out role shows solid black with the game holding LED ownership at all times: actual behavior is solid black during gameplay (the game still owns the LEDs for judgement effects), and outside gameplay an empty pad is handed back to the pad firmware's built-in lighting.

Testing

  • cargo test -p deadsync-smx: 76 passed (includes the new background_declared_empty test).
  • cargo build clean.
  • Manual (options display fix): select a pack on the SMX options page, leave the screen, re-enter; the row now shows the selected pack instead of Default.

@fchorney fchorney changed the title fix(smx): show the saved gif pack selection on the options screen fix(smx): gif pack option display and CanBeEmpty fallback semantics Jul 9, 2026
fchorney added 2 commits July 10, 2026 10:02
The SmxBgPack and SmxJudgePack rows hold a single static placeholder
choice; the real list (Default plus discovered packs) is provided
dynamically by the options layout. The screen-init sync went through
set_choice_by_id, which clamps the index to the static choice list
length, forcing the displayed selection to 0. The rows therefore always
showed Default when re-entering the options screen, even though the
saved config and the actual pad lighting kept the chosen pack.

Write the index directly via get_choice_by_id_mut instead, matching the
noteskin and software-renderer-threads rows that use the same
dynamic-choices pattern.
CanBeEmpty exists so a pack can say a role should never have an
animation. The registry honored that for the pack-level chain (declared
Fallback pack, automatic common fallback), but the app's screen
resolution then tried the default role, so a pack that declared
CanBeEmpty=gameplay while supplying its own default gif still showed
that default gif during gameplay.

Walk the role candidates (grade-specific results roles, the screen role,
then default) in order and stop dead at any candidate the selected pack
declares under CanBeEmpty without supplying: no animation resolves at
all. A supplied gif still wins over its own declaration, and per-song /
per-pack scoped gifs sit above pack policy and are unaffected.

Also aligns the docs with the actual empty behavior: during gameplay the
game keeps LED ownership so an empty background is solid black; on other
screens an empty pad is handed back to the pad firmware's built-in
lighting.
@fchorney fchorney force-pushed the fc/smx-pack-row-display branch from 3cfc44d to 417846b Compare July 10, 2026 15:05
@fchorney

fchorney commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Heads up: main currently doesn't build on macOS

Not caused by this PR, and not fixed by it. Fix is up as #618, which branches straight off main and is independent of this one.

I hit these while rebasing onto main (146d8241). Both are inside #[cfg(target_os = "macos")] blocks, which is presumably why CI is green.

1. crates/deadlib-renderer/src/window_size.rs:12 (from de4a1c5b)

const fn macos_opengl_low_dpi(backend_type: BackendType, high_dpi: bool) -> bool {
    backend_type == BackendType::OpenGL && !high_dpi
}
error[E0015]: cannot call non-const operator in constant functions

== on BackendType goes through PartialEq::eq, which isn't callable in a const fn on stable (it needs const_trait_impl). #618 uses matches!(backend_type, BackendType::OpenGL) instead: pattern matching calls no trait method, so the function stays const.

2. crates/deadsync-shell/src/window.rs:95 (from 146d8241)

attributes = attributes.with_disallow_hidpi(!config.high_dpi);
error[E0599]: no method named `with_disallow_hidpi` found for struct `WindowAttributes`

The method does exist in winit 0.30.13; its trait is not in scope here. WindowAttributesExtMacOS is imported in window_size.rs, where this call used to live, but the call moved into window.rs during 146d8241 without its import. #618 imports the trait, cfg-gated to match the call site, and drops it from window_size.rs, which no longer uses it.

Reproduced on main with a clean worktree, stable rustc 1.96.0, aarch64-apple-darwin.

Edited: an earlier revision of this comment misdiagnosed (2) as the method being absent from winit 0.30.13. It is present; the trait was simply not imported.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant