Skip to content

Release 4.3.x#204

Open
logicallysynced wants to merge 7 commits into
masterfrom
chromatics-4.x
Open

Release 4.3.x#204
logicallysynced wants to merge 7 commits into
masterfrom
chromatics-4.x

Conversation

@logicallysynced

Copy link
Copy Markdown
Owner

Brings master from 4.1.38 to 4.3.1 - 475 commits covering the 4.1.44 to 4.2.73 releases plus the 4.3.1 audit pass. Everything here has already shipped to users through the Velopack update channel except 4.3.1, which is the release this PR prepares.

What each version added

  • 4.1.44: QMK Raw HID keyboard support (Beta) with a pre-built layout database covering 2650 boards.
  • 4.2.50: Windows Dynamic Lighting, Yeelight and Alienware LightFX support (all Beta), layer copying between devices, Hue/LIFX/Yeelight in the first-run wizard, multi-zone keyboard layouts, code-signed installers, and a batch of fixes (Vegas Mode in the Gold Saucer, title animation during zone loads, clipboard loss on close).
  • 4.2.65: EVision keyboard support (13 boards on the Sonix VS11K28A firmware), Redragon mouse support (13 models), updated Corsair/CoolerMaster/MSI/Wooting SDKs, and a fix for Corsair and OpenRGB providers failing to start.
  • 4.2.73: FFXIV 7.51 support, layer remapping on keyboard-type change, AZERTY fixes, a crash fix when changing layer modes, and two Enmity Tracker fixes (dark while engaged, dark after disable/re-enable).
  • 4.3.1: a codebase audit. 10 subsystem auditors produced 67 findings; adversarial verification confirmed 47, and 44 were fixed (the other 3 are credential-handling items in private build scripts, handled outside the repo). Details below.

The 4.3.1 audit, by area

Thread safety

  • _runningEffects and _activeDevices in RGBController were plain collections written from device-SDK callback threads while the game loop and UI thread read them. Both now sit behind dedicated locks; teardown paths snapshot before detaching.
  • _layergroups was a plain Dictionary mutated by the game loop while the RGB.NET timer thread read it through surface.Updating. It is now a ConcurrentDictionary, with all 24 processor write sites and 4 removal sites converted.
  • Strobe and Pulse decorators could crash the render loop when detached mid-update. Both now carry the null guards and try/catch the Starfield family already had.
  • The Sentry heartbeat blocked a thread-pool thread with Thread.Sleep(1500) every minute. It is now async end to end.

Logic bugs

  • DamageFlash clamped every minimum-opacity value below 1.0 to zero, which disabled the scale-damage-flash setting for its entire useful range. The clamp now floors at 0.
  • ArenaLightShow blended a 0-1 base color into a 0-255 channel scale, so the configured base color was invisible and the effect faded to black. Base channels are now scaled to match.
  • Blacksmith characters got Black Mage palette colors from a copy-paste error in GameHelper.
  • A missing assignment in DictionaryConverter threw NullReferenceException during settings deserialization on the fallback path.
  • Yeelight marked colors as delivered even when the TCP send failed, so one Wi-Fi blip stopped a bulb painting until the provider was toggled. Send state now updates only on success, failures retry next frame, and errors surface once per outage instead of once per retry.
  • The Hue adoption dialog mutated an ObservableCollection from a thread-pool thread after ConfigureAwait(false). Continuations now resume on the UI thread.
  • TargetCastbar was the only processor missing its _disposed guard.
  • Palette, effect and settings saves wrote directly to the target file, so a crash mid-write corrupted them. All three now use the same atomic tmp-and-replace pattern layer mappings already used.

Performance

  • JobGaugeA/B/C rebuilt every LED group on every game tick in Interpolate mode. They now rebuild only when the gauge value changes, matching the guard HPTracker and the other trackers already had.
  • Per-tick allocations removed across nine layer processors: cached brushes in Highlight, both BattleStances, JobClassesHighlight, JobClasses and TargetCastbar; a persistent dictionary in Keybinds; DamageFlash now allocates its decorator only when a flash actually fires; CutsceneAnimation allocated a StarfieldDecorator every tick and never used it.
  • Per-frame buffer allocations removed in the DynamicLighting, Alienware, EVision, Redragon and QMK update queues, with wire output verified unchanged.
  • LIFX and QMK held their device lock through inter-chunk pacing sleeps (up to 90ms for a 70-zone LIFX strip), which froze the UI thread when re-enabling a device from the Mappings tab. Packets are now built under the lock and sent with identical pacing outside it.
  • GetLedArray ran an O(n*m) LINQ scan per frame; it now uses a HashSet.

Resource leaks

  • LayerItemViewModel subscribed to the static localization event and never unsubscribed, leaking a full set of layer items on every refresh of the Mappings tab. The three device-adoption dialog view models had the same leak. All four now unsubscribe on disposal.
  • The Yeelight Music Mode listener leaked its pending accept task and any late-arriving socket.
  • SettingsViewModel held a registry key open for the life of the app; it is now scoped to each call.

Dead code

  • The GameLoop CPU-throttle branch could never execute (its flag was set false on the line above the test) and is removed, along with a helper that read a memory counter and returned it as a CPU percentage.

Two findings needed no change on inspection (one was already in the recommended form, one targets an app-lifetime object where unsubscription buys nothing), and the harness gained a fix of its own: effect presets now round-trip all 28 parameters instead of a third of them.

Every fix was re-reviewed against the diff before commit. That second pass caught and corrected two regressions in the first round of fixes (a snapshot return that broke effect registration, and a stale-buffer repaint in Dynamic Lighting), one log-spam path, and one compile error.

CI

Verification

  • Release build of all three projects: 0 warnings, 0 errors.
  • Full test suite: 133/133 passing.
  • Working tree grepped for signing-cert identity strings before each commit: clean.

🤖 Generated with Claude Code

logicallysynced and others added 2 commits June 10, 2026 20:23
…(v4.2.73 -> v4.3.1)

Multi-agent audit of the full codebase (67 raw findings, adversarially
verified down to 47; 3 build-script credential items handled separately
by the owner). Fixes by area:

Core: lock discipline for _runningEffects and _activeDevices,
_layergroups swapped to ConcurrentDictionary (game loop vs RGB.NET
timer thread race), dead CPU-throttle branch removed, Sentry heartbeat
no longer blocks a thread-pool thread.

Layers: DamageFlash min-opacity clamp inverted the whole (0,1) range,
JobGaugeA/B/C interpolate mode rebuilt LED groups every tick, missing
_disposed guard in TargetCastbar, per-tick brush and dictionary
allocations hoisted across nine processors, CutsceneAnimation dead
decorator allocation removed, GetLedArray O(n*m) scan now a HashSet.

Decorators: ArenaLightShow blended a 0-1 base color into a 0-255 scale
(base color invisible), Strobe/Pulse detach race guards,
RectangularGradient bottom-edge interpolation typo.

Devices: Yeelight dedup state no longer poisoned by failed sends (bulb
recovers after Wi-Fi drops, errors surface once per outage), Music Mode
accept task leak fixed, LIFX and QMK inter-chunk sleeps moved outside
the device lock (UI no longer freezes on multizone re-enable),
per-frame buffer allocations removed in DynamicLighting, Alienware,
EVision, Redragon and QMK queues.

Helpers: settings deserialization NRE in DictionaryConverter,
Blacksmith job used Black Mage palette colors, palette/effects/settings
saves now atomic (tmp + replace), GetMaxCpuUsage read a memory counter.

UI: Hue adoption dialog mutated ObservableCollection off the UI thread,
LocalizationService subscription leaks in layer items and adoption
dialogs, console trim no longer fires 500 list shifts, registry key in
SettingsViewModel now scoped and disposed.

Harness: SavePreset now round-trips all 28 effect parameters.

Build clean, 133/133 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…203)

The retry v4 major only moves the action to the node24 runtime; inputs
and retry behaviour are unchanged, and windows-latest runners ship
node24. Applied here so the release branch carries it; dependabot
closes #203 once this reaches master.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@logicallysynced logicallysynced self-assigned this Jun 10, 2026
…tion (v4.3.1 -> v4.3.2)

Sharlayan 9.1.1 is the full-codebase-audit release (74 verified
fixes plus an FFXIVClientStructs submodule refresh, FFXIVAPP/sharlayan
PR #116). Referenced via NuGet in both Chromatics.csproj and
Chromatics.Tests.csproj as before.

Compile fixes for the one breaking change Chromatics touches:
AstrologianResources.DrawnCards went from List<AstrologianCard> to
AstrologianCard[], so the two .Count reads became .Length -
JobGaugeB.cs (AST next-card gauge) and JobGaugeC.cs (AST draw-state
gauge). The [0] indexer and null checks compile unchanged.

Verified non-impacts per the migration notes:
- ActionContainers / InventoryContainers ConcurrentBag -> List:
  Keybinds.cs uses null check + foreach only, compiles unchanged.
  The ConcurrentBag in LifxDiscovery.cs is Chromatics' own and
  unrelated.
- DancerResources.Steps -> DanceStep[]: no Chromatics usage.
- MonkResources.BeastChakra now get-only: no Chromatics usage
  (JobMNKBeastChakra is a palette entry, not the Sharlayan type).
- MateriaType ushort-backed: no (byte) casts anywhere in Chromatics.

User-visible wins inherited from the Sharlayan fixes, called out in
the 4.3.2 CHANGELOG entry:
- RPR / SGE levels and EXP now populate, so the Experience Tracker
  layer works for Reaper and Sage (previously read 0).
- Hotbar action ids above 32767 read correctly, fixing Keybinds
  layer dispatch on high-id actions.
- Party member Coordinate.Y no longer mirrors Z.

Build clean, 133/133 xUnit tests pass.
… forward-compat guard, schemaVersion 6 (v4.3.2 -> v4.3.3)

Two new dynamic layer types mirroring TargetHP / TargetCastbar but
reading TargetInfo.FocusTarget instead of CurrentTarget:

- DynamicLayerType.FocusTargetHP = 16, FocusTargetCastbar = 17.
  Enum values equal their _dynamicLayerOrder positions per the
  position == enum-value invariant; both order arrays
  (LayerItemViewModel + CopyLayersDialogViewModel) got the matching
  appends and LayerProcessorFactory.CreateDynamicProcessor got the
  two cases. The stale DynamicLayerProcessorFactory map in
  LayerProcessor.cs has no callers (it already lacks JobGaugeC) and
  was left untouched.
- FocusTargetHPProcessor clones the TargetHP structure: per-layerID
  model, claimed / friendly / idle brush selection from InCombat +
  IsAggressive, empty bar painted when nothing is focus targeted,
  Interpolate bar fill + Fade blend with the requestUpdate rebuild
  guard.
- FocusTargetCastbarProcessor clones TargetCastbar but fixes the
  no-target path while at it: instead of the early return that
  freezes the last cast frame, it tracks the focus-target id, casts
  at 0.0 with no focus target, and repaints the empty bar on target
  change. Same requestUpdate guard on both modes.
- Six new palette entries under Target/Enemy: Focus Target Cast Bar
  Charge Build / Empty and Focus Target HP Friendly / Claimed /
  Empty / Idle. Palette files saved by older versions load fine -
  missing fields fall back to the field initialisers.
- LayerDisplay names + descriptions localised: 4 new en.json keys,
  all six locale files regenerated via translate.py.

Forward-compat guard (per request): layers.chromatics4 written by a
newer Chromatics can carry dynamic positions this build's enum
doesn't define. MappingLayers.SanitizeLayerTypeIndexes validates
layerTypeindex against the matching enum for the layer's
rootLayerType and resets unknown positions to 0 (None) with a
console line naming the layer. Runs in LoadMappings (startup path)
and ImportMappings (Mapping-tab import + migration path). Without
it, an unknown position flows into GameController's
(DynamicLayerType) cast and the processor factory throws on every
frame. schemaVersion bumped 5 -> 6 in MappingFileV3,
SaveLayerMappings, and ExportLayerMappings to mark files that can
carry the new positions.

Build clean, 133/133 xUnit tests pass.
…maps (v4.3.3 -> v4.3.4)

Neither static factory map had a single caller - GameController
dispatches base and dynamic layers through
LayerProcessorFactory.GetProcessor. Both maps had also drifted out
of date: BaseLayerProcessorFactory was missing AudioVisualizer,
DynamicLayerProcessorFactory was missing JobGaugeC and the new
focus-target layers. Worse than harmless, their static initialisers
eagerly built every processor singleton the moment anything touched
the class.

EffectLayerProcessorFactory stays - GameController iterates its map
for effect-layer dispatch (GameController.cs:744). A comment above
it records why its two siblings are gone so they don't get
reintroduced by pattern-matching.

CHANGELOG header renamed 4.3.3 -> 4.3.4 to track the version (also
fixes the "Castbasr" typo in the focus-target bullet); the existing
"Various performance improvements and bug fixes" line covers this
cleanup.

Build clean, 133/133 xUnit tests pass.
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