@@ -379,6 +379,13 @@ private bool OnSetRumbleState(uint port, uint effect, ushort strength)
379379 private byte [ ] ? _pendingLoadData = null ;
380380 private string _pendingLoadName = "" ;
381381 private string ? _pendingLoadStatePath = null ; // load on startup if set
382+ // Frame counter for the startup-state retry loop. Some cores (Beetle PSX,
383+ // PCSX-ReARMed, Saturn) report retro_serialize_size differently during
384+ // BIOS boot vs. once the game is running, so retro_unserialize fails on
385+ // the first frame and would leave the user stuck in BIOS. We retry each
386+ // frame until success or this cap (~10s @60fps).
387+ private int _loadStateAttempts = 0 ;
388+ private const int MaxLoadStateAttempts = 600 ;
382389 // Cheats — loaded once per game from disk, applied after retro_load_game and after every state load.
383390 private System . Collections . Generic . List < Models . Cheat > _cheats = new ( ) ;
384391 private bool _cheatsApplied = false ;
@@ -1691,16 +1698,29 @@ private void StartEmulator()
16911698 // between retro_run calls (after the first frame). Calling retro_unserialize before
16921699 // any retro_run has executed is not safe — the core may not be at a consistent
16931700 // checkpoint yet (mupen64plus starts its own EmuThread during retro_load_game).
1694- if ( _pendingLoadStatePath != null && File . Exists ( _pendingLoadStatePath ) )
1701+ if ( _pendingLoadStatePath != null )
16951702 {
1696- try
1703+ if ( File . Exists ( _pendingLoadStatePath ) )
1704+ {
1705+ try
1706+ {
1707+ _pendingLoadData = File . ReadAllBytes ( _pendingLoadStatePath ) ;
1708+ _pendingLoadName = Path . GetFileNameWithoutExtension ( _pendingLoadStatePath ) ;
1709+ _loadStatePending = true ;
1710+ System . Diagnostics . Trace . WriteLine ( $ "Queued pending state load: { _pendingLoadStatePath } ") ;
1711+ }
1712+ catch ( Exception ex ) { System . Diagnostics . Trace . WriteLine ( $ "Pending load read failed: { ex . Message } ") ; }
1713+ }
1714+ else
16971715 {
1698- _pendingLoadData = File . ReadAllBytes ( _pendingLoadStatePath ) ;
1699- _pendingLoadName = Path . GetFileNameWithoutExtension ( _pendingLoadStatePath ) ;
1700- _loadStatePending = true ;
1701- System . Diagnostics . Trace . WriteLine ( $ "Queued pending state load: { _pendingLoadStatePath } ") ;
1716+ // Surface this loudly — if a save state's .state file went missing
1717+ // (Defender quarantine, accidental delete, etc.), the launch
1718+ // silently used to fall through to a fresh BIOS boot. Now it shows.
1719+ string missing = _pendingLoadStatePath ;
1720+ System . Diagnostics . Trace . WriteLine ( $ "Pending load skipped — file not found: { missing } ") ;
1721+ _transientMsg = $ "Save state file missing: { Path . GetFileName ( missing ) } ";
1722+ _transientExpiry = DateTime . Now . AddSeconds ( 6 ) ;
17021723 }
1703- catch ( Exception ex ) { System . Diagnostics . Trace . WriteLine ( $ "Pending load read failed: { ex . Message } ") ; }
17041724 _pendingLoadStatePath = null ;
17051725 }
17061726
@@ -3252,6 +3272,19 @@ private bool OnEnvironmentBody(uint cmd, uint baseCmd, IntPtr data)
32523272 case RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS :
32533273 if ( ( cmd & 0x10000 ) != 0 )
32543274 return _consoleHandler . AllowHwSharedContext ;
3275+ // Acknowledge variable-size save states by OR-ing
3276+ // RETRO_SERIALIZATION_QUIRK_FRONT_VARIABLE_SIZE (1<<3)
3277+ // into the core's flags. Beetle PSX gates
3278+ // `enable_variable_serialization_size` on this — without
3279+ // the ack the core reports a stub serialize_size that
3280+ // never matches a real on-disc state, so resuming from
3281+ // a save state silently fails and BIOS boots instead.
3282+ if ( data != IntPtr . Zero )
3283+ {
3284+ const ulong FRONT_VARIABLE_SIZE = 1UL << 3 ;
3285+ ulong coreFlags = ( ulong ) Marshal . ReadInt64 ( data ) ;
3286+ Marshal . WriteInt64 ( data , ( long ) ( coreFlags | FRONT_VARIABLE_SIZE ) ) ;
3287+ }
32553288 return true ;
32563289
32573290 case RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE :
@@ -5354,91 +5387,136 @@ private void FinalizeSave(string name, byte[] data,
53545387
53555388 File . WriteAllBytes ( statePath , data ) ;
53565389
5357- // Screenshot — HW cores pre-capture pixels on emu thread; SW cores capture from bitmap on UI thread below.
5358- if ( ! isHw || ( screenshotPixels != null && ssWidth > 0 && ssHeight > 0 ) )
5390+ // Screenshot — try in order:
5391+ // 1. HW cores: pre-captured _hwFlippedBuffer pixels (readback path)
5392+ // 2. SW cores: WPF WriteableBitmap _bitmap (the source-of-truth frame)
5393+ // 3. Fallback for either path when (1)/(2) is empty: RenderTargetBitmap
5394+ // of the GameScreen control on the UI thread. Works whenever the
5395+ // core renders into the WPF visual tree (i.e. not a native Vulkan/GL
5396+ // overlay window). Beats silently saving no PNG.
5397+ BitmapSource ? bmp = null ;
5398+ try
53595399 {
5360- try
5400+ if ( isHw )
53615401 {
5362- BitmapSource bmp ;
5363- if ( isHw )
5402+ if ( screenshotPixels != null && ssWidth > 0 && ssHeight > 0 )
53645403 {
53655404 bmp = BitmapSource . Create ( ( int ) ssWidth , ( int ) ssHeight ,
53665405 96 , 96 , PixelFormats . Bgra32 , null , screenshotPixels ,
53675406 ( int ) ssWidth * 4 ) ;
53685407 }
53695408 else
53705409 {
5371- // Software core: capture from WPF WriteableBitmap on UI thread
5372- byte [ ] ? swPixels = null ;
5373- int swW = 0 , swH = 0 , swStride = 0 ;
5374- Dispatcher . Invoke ( ( ) =>
5410+ System . Diagnostics . Trace . WriteLine (
5411+ $ "Screenshot HW path skipped — readback empty (buf={ screenshotPixels ? . Length ?? 0 } , { ssWidth } x{ ssHeight } ). Trying GameScreen fallback.") ;
5412+ }
5413+ }
5414+ else
5415+ {
5416+ // Software core: capture from WPF WriteableBitmap on UI thread
5417+ byte [ ] ? swPixels = null ;
5418+ int swW = 0 , swH = 0 , swStride = 0 ;
5419+ Dispatcher . Invoke ( ( ) =>
5420+ {
5421+ if ( _bitmap != null )
5422+ {
5423+ swW = _bitmap . PixelWidth ; swH = _bitmap . PixelHeight ;
5424+ swStride = _bitmap . BackBufferStride ; // actual stride (Bgr565 = swW*2, not swW*4)
5425+ swPixels = new byte [ swH * swStride ] ;
5426+ _bitmap . CopyPixels ( swPixels , swStride , 0 ) ;
5427+ }
5428+ } ) ;
5429+ if ( swPixels != null && swW > 0 )
5430+ {
5431+ if ( _pixelFormat == RETRO_PIXEL_FORMAT_XRGB8888 )
5432+ {
5433+ // Bgr32 raw data: bytes are [B, G, R, X] where X=0.
5434+ // Set X→0xFF so BitmapSource.Create(Bgra32) gets fully opaque alpha.
5435+ for ( int i = 3 ; i < swPixels . Length ; i += 4 )
5436+ swPixels [ i ] = 0xFF ;
5437+ }
5438+ else if ( _pixelFormat == RETRO_PIXEL_FORMAT_RGB565 )
53755439 {
5376- if ( _bitmap != null )
5440+ // Convert Bgr565 → Bgra32.
5441+ // Must index by row×stride+col×2 because stride ≠ swW*2 in general.
5442+ var bgra = new byte [ swW * swH * 4 ] ;
5443+ for ( int y = 0 ; y < swH ; y ++ )
5444+ for ( int x = 0 ; x < swW ; x ++ )
53775445 {
5378- swW = _bitmap . PixelWidth ; swH = _bitmap . PixelHeight ;
5379- swStride = _bitmap . BackBufferStride ; // actual stride (Bgr565 = swW*2, not swW*4)
5380- swPixels = new byte [ swH * swStride ] ;
5381- _bitmap . CopyPixels ( swPixels , swStride , 0 ) ;
5446+ int src = y * swStride + x * 2 ;
5447+ ushort px = ( ushort ) ( swPixels [ src ] | ( swPixels [ src + 1 ] << 8 ) ) ;
5448+ int dst = ( y * swW + x ) * 4 ;
5449+ bgra [ dst + 0 ] = ( byte ) ( ( px & 0x1F ) * 255 / 31 ) ;
5450+ bgra [ dst + 1 ] = ( byte ) ( ( ( px >> 5 ) & 0x3F ) * 255 / 63 ) ;
5451+ bgra [ dst + 2 ] = ( byte ) ( ( px >> 11 ) * 255 / 31 ) ;
5452+ bgra [ dst + 3 ] = 0xFF ;
53825453 }
5383- } ) ;
5384- if ( swPixels != null && swW > 0 )
5454+ swPixels = bgra ; swStride = swW * 4 ;
5455+ }
5456+ bmp = BitmapSource . Create ( swW , swH , 96 , 96 , PixelFormats . Bgra32 , null , swPixels , swStride ) ;
5457+ }
5458+ else
5459+ {
5460+ System . Diagnostics . Trace . WriteLine (
5461+ $ "Screenshot SW path skipped — _bitmap unavailable (pixels={ swPixels ? . Length ?? 0 } , { swW } x{ swH } ). Trying GameScreen fallback.") ;
5462+ }
5463+ }
5464+
5465+ // Fallback: RenderTargetBitmap of the GameScreen Image control.
5466+ if ( bmp == null )
5467+ {
5468+ Dispatcher . Invoke ( ( ) =>
5469+ {
5470+ try
53855471 {
5386- if ( _pixelFormat == RETRO_PIXEL_FORMAT_XRGB8888 )
5472+ int w = ( int ) Math . Round ( GameScreen . ActualWidth ) ;
5473+ int h = ( int ) Math . Round ( GameScreen . ActualHeight ) ;
5474+ if ( w > 0 && h > 0 )
53875475 {
5388- // Bgr32 raw data: bytes are [B, G, R, X] where X=0.
5389- // Set X→0xFF so BitmapSource.Create(Bgra32) gets fully opaque alpha.
5390- for ( int i = 3 ; i < swPixels . Length ; i += 4 )
5391- swPixels [ i ] = 0xFF ;
5476+ var rtb = new RenderTargetBitmap ( w , h , 96 , 96 , PixelFormats . Pbgra32 ) ;
5477+ rtb . Render ( GameScreen ) ;
5478+ rtb . Freeze ( ) ;
5479+ bmp = rtb ;
53925480 }
5393- else if ( _pixelFormat == RETRO_PIXEL_FORMAT_RGB565 )
5481+ else
53945482 {
5395- // Convert Bgr565 → Bgra32.
5396- // Must index by row×stride+col×2 because stride ≠ swW*2 in general.
5397- var bgra = new byte [ swW * swH * 4 ] ;
5398- for ( int y = 0 ; y < swH ; y ++ )
5399- for ( int x = 0 ; x < swW ; x ++ )
5400- {
5401- int src = y * swStride + x * 2 ;
5402- ushort px = ( ushort ) ( swPixels [ src ] | ( swPixels [ src + 1 ] << 8 ) ) ;
5403- int dst = ( y * swW + x ) * 4 ;
5404- bgra [ dst + 0 ] = ( byte ) ( ( px & 0x1F ) * 255 / 31 ) ;
5405- bgra [ dst + 1 ] = ( byte ) ( ( ( px >> 5 ) & 0x3F ) * 255 / 63 ) ;
5406- bgra [ dst + 2 ] = ( byte ) ( ( px >> 11 ) * 255 / 31 ) ;
5407- bgra [ dst + 3 ] = 0xFF ;
5408- }
5409- swPixels = bgra ; swStride = swW * 4 ;
5483+ System . Diagnostics . Trace . WriteLine (
5484+ $ "Screenshot fallback skipped — GameScreen size { w } x{ h } ") ;
54105485 }
5411- bmp = BitmapSource . Create ( swW , swH , 96 , 96 , PixelFormats . Bgra32 , null , swPixels , swStride ) ;
54125486 }
5413- else
5487+ catch ( Exception fbEx )
54145488 {
5415- pngPath = "" ;
5416- bmp = null ! ;
5489+ System . Diagnostics . Trace . WriteLine ( $ "Screenshot fallback failed: { fbEx . Message } ") ;
54175490 }
5418- }
5491+ } ) ;
5492+ }
54195493
5420- if ( bmp != null )
5494+ if ( bmp != null )
5495+ {
5496+ // Rotate screenshot to match display orientation (vertical arcade games etc.)
5497+ if ( coreRotation != 0 )
54215498 {
5422- // Rotate screenshot to match display orientation (vertical arcade games etc.)
5423- if ( coreRotation != 0 )
5424- {
5425- double angle = ( ( - ( int ) coreRotation * 90.0 ) % 360 + 360 ) % 360 ;
5426- bmp = new TransformedBitmap ( bmp , new RotateTransform ( angle ) ) ;
5427- }
5428- bmp . Freeze ( ) ;
5429- using var fs = new FileStream ( pngPath , FileMode . Create ) ;
5430- var enc = new PngBitmapEncoder ( ) ;
5431- enc . Frames . Add ( BitmapFrame . Create ( bmp ) ) ;
5432- enc . Save ( fs ) ;
5499+ double angle = ( ( - ( int ) coreRotation * 90.0 ) % 360 + 360 ) % 360 ;
5500+ bmp = new TransformedBitmap ( bmp , new RotateTransform ( angle ) ) ;
54335501 }
5502+ if ( bmp . CanFreeze && ! bmp . IsFrozen ) bmp . Freeze ( ) ;
5503+ using var fs = new FileStream ( pngPath , FileMode . Create ) ;
5504+ var enc = new PngBitmapEncoder ( ) ;
5505+ enc . Frames . Add ( BitmapFrame . Create ( bmp ) ) ;
5506+ enc . Save ( fs ) ;
5507+ System . Diagnostics . Trace . WriteLine ( $ "Screenshot saved: { pngPath } ") ;
54345508 }
5435- catch ( Exception ex )
5509+ else
54365510 {
5437- System . Diagnostics . Trace . WriteLine ( $ "Screenshot failed: { ex . Message } ") ;
5511+ System . Diagnostics . Trace . WriteLine ( $ "Screenshot not saved — every capture path returned empty for { safeName } ") ;
54385512 pngPath = "" ;
54395513 }
54405514 }
5441- else pngPath = "" ;
5515+ catch ( Exception ex )
5516+ {
5517+ System . Diagnostics . Trace . WriteLine ( $ "Screenshot failed: { ex . Message } ") ;
5518+ pngPath = "" ;
5519+ }
54425520 var meta = new
54435521 {
54445522 Name = name ,
@@ -5512,15 +5590,36 @@ private void RequestLoad(string statePath, string name)
55125590 /// <summary>Called on the emu thread between retro_run calls.</summary>
55135591 private void ExecuteLoadOnEmuThread ( )
55145592 {
5515- _loadStatePending = false ;
55165593 byte [ ] ? data = _pendingLoadData ;
55175594 string name = _pendingLoadName ;
5518- _pendingLoadData = null ;
55195595
5520- if ( data == null ) return ;
5596+ if ( data == null )
5597+ {
5598+ _loadStatePending = false ;
5599+ _loadStateAttempts = 0 ;
5600+ return ;
5601+ }
5602+
55215603 bool ok = _core ? . LoadState ( data ) ?? false ;
5604+
5605+ // Retry across frames if the core hasn't reached a state where
5606+ // retro_unserialize will accept this snapshot yet (typically PSX
5607+ // cores during BIOS boot — serialize_size doesn't stabilize for
5608+ // dozens of frames). Bail with an error after ~10 seconds.
5609+ if ( ! ok && _loadStateAttempts < MaxLoadStateAttempts )
5610+ {
5611+ _loadStateAttempts ++ ;
5612+ return ;
5613+ }
5614+
5615+ _loadStatePending = false ;
5616+ _loadStateAttempts = 0 ;
5617+ _pendingLoadData = null ;
5618+
55225619 _transientMsg = ok ? $ "Loaded: { name } " : $ "Failed to load: { name } ";
55235620 _transientExpiry = DateTime . Now . AddSeconds ( 3 ) ;
5621+ System . Diagnostics . Trace . WriteLine (
5622+ $ "[LoadState] { ( ok ? "succeeded" : "gave up" ) } after { ( ok ? _loadStateAttempts : MaxLoadStateAttempts ) } attempts: { name } ") ;
55245623
55255624 // Some cores wipe their cheat table on state load — re-apply so codes survive.
55265625 // Snapshot the list before iterating to avoid racing the UI thread, which can
0 commit comments