LegacyLedEncoder fills a single pooled report buffer from two threads — the LED refresh task and Clear(), called from SimHub's manager thread — with no lock. This is a latent hazard rather than an observed corruption: Clear() waits up to two seconds on the refresh task first, so overlap requires that wait to time out. But nothing guarantees exclusion, and the failure mode is a torn frame.
DisplayEncoder locks this exact pattern deliberately, with a comment noting that the transport lock alone still allows torn frames. The col03 LedEncoder has the same unguarded pattern, so a fix should cover both encoders rather than only the legacy one.
Fix: hold the lock across both report construction and transport submission — locking only the fill still allows a second thread to overwrite the buffer before the first thread's write is issued.
LegacyLedEncoderfills a single pooled report buffer from two threads — the LED refresh task andClear(), called from SimHub's manager thread — with no lock. This is a latent hazard rather than an observed corruption:Clear()waits up to two seconds on the refresh task first, so overlap requires that wait to time out. But nothing guarantees exclusion, and the failure mode is a torn frame.DisplayEncoderlocks this exact pattern deliberately, with a comment noting that the transport lock alone still allows torn frames. The col03LedEncoderhas the same unguarded pattern, so a fix should cover both encoders rather than only the legacy one.Fix: hold the lock across both report construction and transport submission — locking only the fill still allows a second thread to overwrite the buffer before the first thread's write is issued.