Repository navigation
Conversation
PinMAME can run lamps, GI and solenoids through the game driver's device models (#44 lamp bulbs, #89 flasher bulbs, LEDs, two-state solenoids) and report 0-255 levels, which VPX tables get with UseVPMModSol = 2. VPE never turned it on, so lamps were 0/1, GI 0-8 and flashers on/off. Add a "Physical Outputs" flags field (Lamps, GI, Solenoids; default none) to the PinMAME gamelogic engine: - Applied right after ApplyConfig (which resets PinMAME's options) with PinmameSetSolenoidMask(2, mask) and read back. pinmame-dotnet 1.1.0 has no wrapper, so a guarded internal DllImport with the binding's library names is used until it ships one (TODO in the code). - Physical lamp/GI levels are scaled into the range of SingleFading mappings (value * FadingSteps / 255), so the default GI mapping (8 steps) gets the full 0-255 resolution without remapping the table. - The fades of lights driven by physical outputs are turned off for the session, since PinMAME's bulb model already fades them. - Physical solenoids: libpinmame integrates physical outputs on request only (core.c VIDEO_UPDATE(core_gen), "LibPinMAME: on request"). Lamps and GI are integrated by the per-frame GetChangedLamps/GIs poll, but solenoids only arrive through the callback, which compares the last integrated levels, so no coil ever fired. Call PinmameGetChangedSolenoids once per simulation tick (in SetTimeFence) to integrate them, and treat every solenoid value as 0-255 in physical mode. Also clear the GI list before GetChangedGIs: PinMame 1.1.0 appends without clearing, so every GI change since the game started was dispatched again on every frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
| catch (Exception e) { | ||
| Logger.Error(e, "[PinMAME] Updating physical solenoids failed, turning them off."); | ||
| _physicalOutputs &= ~PinMamePhysicalOutputFlags.Solenoids; |
There was a problem hiding this comment.
Fallback leaves physical coils enabled
If the native library lacks PinmameGetChangedSolenoids, startup can still enable physical solenoids because it checks only the other exports. The first poll then clears _physicalOutputs without resetting the native mask. Later ticks skip the poll that physical coils need, so coil events can stop instead of falling back to binary outputs.
Check the required exports before enabling the mask, and restore the native mask on failure. The startup catch needs the same rollback if an earlier native call already succeeded.
| light.FadeSpeedUp = 0f; | ||
| light.FadeSpeedDown = 0f; |
There was a problem hiding this comment.
Light fades outlive the session
DisableLightFades overwrites FadeSpeedUp and FadeSpeedDown without saving or restoring their original values. If Unity keeps scene objects between play sessions, a later run with physical outputs disabled can inherit zero fades.
Save the original values and restore them at shutdown so the promised session-only change does not depend on scene reload.
Knowledge Base Used: Unity editor workflows
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
What VPX does
With
UseVPMModSol = 2,core.vbssetsController.SolMask(2) = 2. PinMAME then reports lamps, GI and solenoids as 0-255 levels from the game driver's device models (wpc.c: #44 bulbs on 18 V for the lamp matrix, #44 on 6.3 V AC for GI, #89 on 20 V for flashers, LEDs, and a two-state model for plain coils). Flashers and inserts ramp up in ~20 ms and glow down over ~150-250 ms like real bulbs, and ROM strobing and PWM come out right without any table-side fade code.What VPE was missing
PinMameGamelogicEngine.cs:851:ApplyConfig()→PinmameSetConfigzeroes PinMAME's options, and nothing sets the physical output mask afterwards. Lamps stay 0/1, GI 0-8 and flashers on/off.SetSolenoidMask(see the binding PR below).core.cVIDEO_UPDATE(core_gen): "LibPinMAME: on request"). The GLE pollsGetChangedLamps/GetChangedGIsevery frame, and those calls integrate, so lamps and GI work. Solenoids only arrive through the callback (core.c"Report changed solenoids"), which compares the last integrated levels. Nothing integrates them, so their level never changes and no coil fires. Reproduced headless with AFM: 0 solenoid callbacks with physical solenoids, against ~1,050 in binary mode.PinMameGamelogicEngine.cs:501:_changedGIsis never cleared, and pinmame-dotnet 1.1.0'sGetChangedGIs(List)appends without clearing (itsGetChangedLamps(List)does clear). So every GI change since game start is dispatched again on every frame. This already happens today with binary GI. With physical GI (0-255, changing every frame) the list would grow by hundreds of entries per second.Change
Lamps,GI,Solenoids. The default is none, so existing tables don't change.ApplyConfig()withPinmameSetSolenoidMask(2, mask)and read back. The inspector's "Everything" (-1) is masked to these three bits.SingleFadingmappings the GLE scales them into the mapping's range (value * FadingSteps / 255). The default PinMAME GI mapping (8 steps) therefore gets full 0-255 resolution, and tables don't need remapping. Inserts already set to 255 steps are unchanged. Other mapping types get 0-255 as is.FadeSpeedUp/Downon theLightComponents mapped to a physical output, and logs how many it changed.SetTimeFence, which the simulation thread calls once per tick, now also callsPinmameGetChangedSolenoidswhile physical solenoids are on. This integrates them (latency below one tick), and the existing callback path then reports them unchanged. Its own result is dropped. A re-entrancy guard covers the stop paths, which also move the fence. In physical mode every solenoid value is treated as 0-255 (_solenoidsModulated), because a single1is a faint level, not "on"._changedGIsbeforeGetChangedGIs(the bug above).DllImport(PinmameSet/GetSolenoidMask,PinmameGetMaxSolenoids,PinmameGetChangedSolenoids). It uses the binding's own library names per platform, so it binds to the same loaded library. If the native lacks them, the GLE logs an error and keeps binary outputs. A TODO points to the binding PR, which addsSetPhysicalOutputs/GetChangedSolenoids.How tested
dotnet build, 0 errors, same as master).VisualPinball.Engine.PinMAME.Test: 30 passed, 2 skipped, as on master.afm_113b):PinmameGetChangedSolenoidsevery 1 ms (what this PR does per tick): the callbacks are back, on the same coils as binary mode. Plain coils report 0/255. In the flasher test, sol 17 (#89) goes 9 → 156 → 244 → 255 in ~50 ms and then 200, 117, 69, 43, 27, 17, 11, 8, 5, 3, 2, 1, 0 over ~240 ms. Binary mode reports 1/0 for the same pulses.DllImportofPinmameSetSolenoidMask(2, 12), GI mappings set to 255 steps, and light fades zeroed. Result: the game played normally (ball launched, 16 flasher coil events), the ROM read back the types sol 17 = 301 (#89), sol 39 = 400 (LED strobe), lamp 11 = 201, GI = 100, and GI brightness varied (lower-playfield mean 59-93 over a short clip). Flashers there still used a Unity-side #89 approximation, because physical solenoids were dead until this fix.Known limitations
CORE_MODOUT_SOL_2_STATE) reports a coil off only 60 ms after its last pulse (core.c,switchDownLatency = 0.060f). Coils driven by the ROM, flippers included, release later than in binary mode. VPX tables withUseVPMModSol = 2get the same PinMAME behaviour. That is whySolenoidsis a separate flag. Lamps + GI alone are safe.core_getAllPhysicSols).SingleOnOfflamp mappings switch on at any glow, including the cool-down tail. UseSingleFadingto see the fade.PinmameGetChangedSolenoidsat 1 kHz from one thread and lamps/GI at 60 Hz from another, which is the threading this PR introduces in the editor. libpinmame guards these calls with a shared lock, and the outputs touched are disjoint.Dependencies
DllImport. Once released, the TODO switches toPinMame.SetPhysicalOutputs/GetChangedSolenoids.Solenoids. Lamps and GI don't need it.🤖 Generated with Claude Code