Skip to content

Commit b42d753

Browse files
QuickNES AuditLibretroAdmin
authored andcommitted
Audit follow-ups: 11 fixes (correctness, perf, robustness)
Omnibus commit covering items A-L from the third round of the audit list. All changes stay within C++98/03 -- no static_assert, no nullptr, no auto, no range-for, no move semantics, no = delete. Correctness =========== A. Nes_Effects_Buffer::SaveAudioBufferState / RestoreAudioBufferState are no longer empty stubs. The base Effects_Buffer now implements them: snapshots all 7 internal Blip_Buffers (via their per-Blip_Buffer Save), the echo/reverb delay-line contents (~40 KB allocated alongside the originals in set_sample_rate, freed in dtor), and the echo_pos/reverb_pos cursors. The Nes_Effects_Buffer override chains to the base and additionally saves the Nes_Nonlinearizer. Before this, runahead with the stereo-panning option silently produced garbage audio on every rollback. B. retro_serialize and retro_unserialize now snapshot/restore the audio buffer state unconditionally, not only when the frontend's FAST_SAVESTATES bit is set. Within-session save->load round-trips now preserve audio bit-for-bit. The is_fast_savestate() helper is removed. Cross-session loads (close emulator, reopen, load file) still fall back to the existing clear_sound_buf() fade-in: the snapshot lives in process-local extra_buffer storage inside each Blip_Buffer, not in the user-provided save state bytes, so a freshly constructed Blip_Buffer has nothing to restore from. To make this safe, every Save site now sets a new extra_valid sentinel and every Restore site bails out when it is false. Without the sentinel, removing the is_fast_savestate guard would have silently zeroed the audio state on every cross-session load. C. Blip_Buffer::extra_buffer grows from 32 longs to 1024. The old size only worked because the typical save point is immediately after read_samples has drained the buffer to the ~18-long impulse-response tail. Any callsite that serializes mid-frame (or any future change that lets samples_avail() exceed 14 between frames) would have silently corrupted audio across fast-savestate restores. Save and Restore now copy samples_avail() + buffer_extra (clamped to the 1024-entry capacity) and Restore zeros the live region beyond it so no 'future' samples written during speculative emulation leak back into the restored read state. Performance =========== D. The 256-entry retro_palette in retro_run is no longer rebuilt every frame. It is regenerated only when frame.palette[] changes (the game writing to PPU palette RAM, which is typically rare -- startup, scene transitions) or when the user picks a different palette (changes palette_index). Saves ~256 RGB->RGB565 conversions per frame in the steady state. E. retro_serialize_size() result is cached after the first call. Some frontends call this every frame during runahead; the previous implementation re-ran a full save_state into a throwaway Mem_Writer each time. The cache is invalidated on retro_load_game and retro_unload_game (different cart -> different mapper -> different serialized size). F. Nes_Emu::save_state and Nes_Emu::load_state used to 'new Nes_State' (~21 KB) and 'delete' it on every call. The libretro path is single-threaded with one Nes_Emu per process, so two file-scope static Nes_State scratch buffers do the job with no heap traffic. G. Nes_Mapper::create() converted from a chain of independent 'if' tests to a single switch. Cosmetic perf, but it also closes the silent footgun where two entries for the same mapperCode would both execute -- the second would have leaked the first. Robustness / style ================== H. Auto_File_Reader::operator= and Auto_File_Writer::operator= no longer launder their destructive transfer through a const_cast on a const& parameter (UB if the source is a real const object). The Data_Reader*/Data_Writer* members are now declared mutable, which is the proper C++98 fix and which the original author evidently considered -- the previous declaration was literally '/* mutable */ Data_Reader* data;'. I. retro_load_game guards against being called twice without an intervening retro_unload_game (which the spec forbids, but well-behaved cores defend against it anyway since leaking the prior emu would also leave the global Multi_Buffer wired to a destroyed audio chain). K. Nes_Mapper::register_state now asserts that the registered state size fits in mapper_state_t (currently 512 bytes). assert() is compiled out under -DNDEBUG so shipping builds pay nothing. If any future mapper grows past the limit, save_state would have silently truncated it; this catches that at the earliest possible point. L. The mono->stereo path in retro_run now names the stereo-frame count instead of relying on a bare 'read_samples >> 1'. J (PAL region support) is deliberately not in this commit -- it is a feature, not a fix, and requires plumbing the pal flag through Nes_Emu and adjusting frame_rate and the APU/DMC period tables. It deserves its own change.
1 parent cac79f1 commit b42d753

12 files changed

Lines changed: 279 additions & 106 deletions

libretro/libretro.cpp

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ static inline float nes_par(unsigned width, unsigned height)
3535

3636
static Nes_Emu *emu;
3737

38+
// Cached result of retro_serialize_size; populated lazily on first call and
39+
// invalidated on every retro_load_game / retro_unload_game.
40+
static size_t cached_state_size = 0;
41+
3842
static retro_video_refresh_t video_cb;
3943
static retro_audio_sample_t audio_cb;
4044
static retro_audio_sample_batch_t audio_batch_cb;
@@ -61,8 +65,6 @@ Silent_Buffer silent_buffer;
6165
Multi_Buffer *current_buffer = NULL;
6266
bool use_silent_buffer = false;
6367

64-
bool is_fast_savestate();
65-
6668
/* ========================================
6769
* Palette additions START
6870
* (lifted from libretro-fceumm)
@@ -1236,14 +1238,30 @@ void retro_run(void)
12361238
static uint16_t video_buffer[Nes_Emu::image_width * Nes_Emu::image_height];
12371239
static uint16_t retro_palette[256];
12381240

1239-
for (unsigned i = 0; i < 256; i++)
1241+
// The 256-entry retro_palette only needs to be rebuilt when either
1242+
// (a) the user picked a different palette (changes current_nes_colors,
1243+
// which we track via palette_index), or (b) the game wrote to PPU
1244+
// palette RAM (changes frame.palette[]). Most frames neither happens,
1245+
// so cache and compare to skip ~256 RGB->RGB565 conversions per frame.
1246+
static short last_frame_palette[Nes_Emu::max_palette_size];
1247+
static int last_retro_palette_index = -1;
1248+
static bool retro_palette_initialized = false;
1249+
if ( !retro_palette_initialized
1250+
|| palette_index != last_retro_palette_index
1251+
|| memcmp(last_frame_palette, frame.palette, sizeof(last_frame_palette)) != 0 )
12401252
{
1241-
const Nes_Emu::rgb_t& rgb = current_nes_colors[frame.palette[i]];
1253+
for (unsigned i = 0; i < 256; i++)
1254+
{
1255+
const Nes_Emu::rgb_t& rgb = current_nes_colors[frame.palette[i]];
12421256
#if defined(ABGR1555)
1243-
retro_palette[i] = ((rgb.blue & 0xf8) << 7) | ((rgb.green & 0xf8) << 2) | ((rgb.red & 0xf8) >> 3);
1257+
retro_palette[i] = ((rgb.blue & 0xf8) << 7) | ((rgb.green & 0xf8) << 2) | ((rgb.red & 0xf8) >> 3);
12441258
#else
1245-
retro_palette[i] = ((rgb.red & 0xf8) << 8) | ((rgb.green & 0xfc) << 3) | ((rgb.blue & 0xf8) >> 3);
1259+
retro_palette[i] = ((rgb.red & 0xf8) << 8) | ((rgb.green & 0xfc) << 3) | ((rgb.blue & 0xf8) >> 3);
12461260
#endif
1261+
}
1262+
memcpy(last_frame_palette, frame.palette, sizeof(last_frame_palette));
1263+
last_retro_palette_index = palette_index;
1264+
retro_palette_initialized = true;
12471265
}
12481266

12491267
for (int y = 0; y < Nes_Emu::image_height; y++)
@@ -1279,7 +1297,13 @@ void retro_run(void)
12791297
audio_batch_cb(out_samples, read_samples);
12801298
}
12811299
else
1282-
audio_batch_cb(samples, read_samples >> 1);
1300+
{
1301+
// Effects_Buffer is already producing interleaved stereo pairs:
1302+
// read_samples returned a count expressed in mono samples, so the
1303+
// stereo frame count delivered to audio_batch_cb is half that.
1304+
long stereo_frames = read_samples >> 1;
1305+
audio_batch_cb(samples, stereo_frames);
1306+
}
12831307
}
12841308
else
12851309
{
@@ -1352,6 +1376,18 @@ bool retro_load_game(const struct retro_game_info *info)
13521376
if (!environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &fmt))
13531377
return false;
13541378

1379+
// Guard against a frontend that calls retro_load_game without a matching
1380+
// retro_unload_game. The spec disallows it but well-behaved cores are
1381+
// defensive about it anyway, since leaking the prior emu would also
1382+
// leave the global Multi_Buffer wired to a destroyed audio chain.
1383+
if (emu)
1384+
{
1385+
emu->close();
1386+
delete emu;
1387+
emu = NULL;
1388+
}
1389+
cached_state_size = 0;
1390+
13551391
emu = new Nes_Emu;
13561392
check_variables();
13571393

@@ -1398,6 +1434,7 @@ void retro_unload_game(void)
13981434
emu->close();
13991435
delete emu;
14001436
emu = 0;
1437+
cached_state_size = 0;
14011438
}
14021439

14031440
unsigned retro_get_region(void)
@@ -1412,48 +1449,42 @@ bool retro_load_game_special(unsigned, const struct retro_game_info *, size_t)
14121449

14131450
size_t retro_serialize_size(void)
14141451
{
1452+
// The serialized size is determined entirely by the loaded cart (mapper
1453+
// state size varies per mapper) and is stable for the life of one
1454+
// retro_load_game. Caching avoids paying the full save_state cost just
1455+
// to measure -- some frontends call this every frame during runahead.
1456+
if (cached_state_size != 0)
1457+
return cached_state_size;
1458+
14151459
Mem_Writer writer;
14161460
if (emu->save_state(writer))
14171461
return 0;
14181462

1419-
return writer.size();
1420-
}
1421-
1422-
bool is_fast_savestate()
1423-
{
1424-
int value;
1425-
bool okay = environ_cb(RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE, &value);
1426-
if (okay)
1427-
{
1428-
if (value & 4)
1429-
{
1430-
return true;
1431-
}
1432-
}
1433-
return false;
1463+
cached_state_size = writer.size();
1464+
return cached_state_size;
14341465
}
14351466

14361467
bool retro_serialize(void *data, size_t size)
14371468
{
1438-
bool isFastSavestate = is_fast_savestate();
14391469
Mem_Writer writer(data, size);
14401470
bool okay = !emu->save_state(writer);
1441-
if (isFastSavestate)
1442-
{
1443-
emu->SaveAudioBufferState();
1444-
}
1471+
// Always snapshot audio buffer state. The snapshot is stored in
1472+
// process-local memory inside each Blip_Buffer (and friends), not in
1473+
// `data`, so within-session save->load round-trips now preserve audio
1474+
// exactly. Cross-session loads (close emulator, reopen, load file)
1475+
// still fall back to the fade-in path because the snapshot doesn't
1476+
// persist in the user buffer -- Restore is gated by an extra_valid
1477+
// sentinel that is false on a freshly constructed Blip_Buffer, so it
1478+
// becomes a no-op and the existing clear_sound_buf() fade kicks in.
1479+
emu->SaveAudioBufferState();
14451480
return okay;
14461481
}
14471482

14481483
bool retro_unserialize(const void *data, size_t size)
14491484
{
1450-
bool isFastSavestate = is_fast_savestate();
14511485
Mem_File_Reader reader(data, size);
14521486
bool okay = !emu->load_state(reader);
1453-
if (isFastSavestate)
1454-
{
1455-
emu->RestoreAudioBufferState();
1456-
}
1487+
emu->RestoreAudioBufferState();
14571488
return okay;
14581489
}
14591490

nes_emu/Blip_Buffer.cpp

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ Blip_Buffer::Blip_Buffer()
4242
extra_offset = offset_;
4343
extra_reader_accum = reader_accum;
4444
memset(extra_buffer, 0, sizeof(extra_buffer));
45+
extra_valid = false;
4546
}
4647

4748
Blip_Buffer::~Blip_Buffer()
@@ -406,15 +407,43 @@ void Blip_Buffer::mix_samples( blip_sample_t const* in, long count )
406407

407408
void Blip_Buffer::SaveAudioBufferState()
408409
{
410+
// Save the live portion of buffer_ (samples_avail + the impulse-response
411+
// tail buffer_extra), bounded by the dedicated extra_buffer's capacity.
412+
// In practice the post-read-samples residual is only ~18 longs, but
413+
// callers may serialize before draining and we want to preserve as much
414+
// as the snapshot area can hold.
415+
long live = samples_avail() + (long) buffer_extra;
416+
if ( live > (long) extra_buffer_size )
417+
live = (long) extra_buffer_size;
409418
extra_length = length_;
410419
extra_offset = offset_;
411420
extra_reader_accum = reader_accum;
412-
memcpy(extra_buffer, buffer_, sizeof(extra_buffer));
421+
if ( live > 0 && buffer_ )
422+
memcpy( extra_buffer, buffer_, (size_t) live * sizeof (buf_t_) );
423+
// Zero any tail to keep deterministic contents in the snapshot.
424+
if ( live < (long) extra_buffer_size )
425+
memset( extra_buffer + live, 0,
426+
(size_t) (extra_buffer_size - live) * sizeof (buf_t_) );
427+
extra_valid = true;
413428
}
414429
void Blip_Buffer::RestoreAudioBufferState()
415430
{
431+
if ( !extra_valid )
432+
return; // no Save has happened in this Blip_Buffer's lifetime
416433
length_ = extra_length;
417434
offset_ = extra_offset;
418435
reader_accum = extra_reader_accum;
419-
memcpy(buffer_, extra_buffer, sizeof(extra_buffer));
436+
if ( buffer_ && buffer_size_ > 0 )
437+
{
438+
long copy = (long) extra_buffer_size;
439+
if ( copy > buffer_size_ )
440+
copy = buffer_size_;
441+
memcpy( buffer_, extra_buffer, (size_t) copy * sizeof (buf_t_) );
442+
// Beyond the snapshot, the buffer may still hold "future" samples
443+
// written by the speculative emulation that ran between Save and
444+
// Restore. Clear them so the restored read state is exact.
445+
if ( copy < buffer_size_ )
446+
memset( buffer_ + copy, 0,
447+
(size_t) (buffer_size_ - copy) * sizeof (buf_t_) );
448+
}
420449
}

nes_emu/Blip_Buffer.h

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,23 @@ class Blip_Buffer {
108108
friend class Blip_Reader;
109109

110110
private:
111-
//extra information necessary to load state to an exact sample
112-
buf_t_ extra_buffer[32];
111+
//extra information necessary to load state to an exact sample.
112+
// Size rationale: between Save and Restore the worst case has the
113+
// entire previous frame's audio still in flight plus the
114+
// impulse-response tail (buffer_extra). At 44.1 kHz/60 fps that's
115+
// ~735 + 18 longs. 1024 leaves comfortable headroom and is still
116+
// only ~8 KB per Blip_Buffer.
117+
enum { extra_buffer_size = 1024 };
118+
buf_t_ extra_buffer[extra_buffer_size];
113119
int extra_length;
114120
long extra_reader_accum;
115121
blip_resampled_time_t extra_offset;
122+
// True iff SaveAudioBufferState has been called since construction.
123+
// RestoreAudioBufferState is a no-op until then so that a fresh
124+
// Blip_Buffer (e.g. after retro_load_game) isn't silently zeroed by
125+
// a frontend that calls retro_unserialize without a matching
126+
// retro_serialize earlier in the same session.
127+
bool extra_valid;
116128
public:
117129
void SaveAudioBufferState();
118130
void RestoreAudioBufferState();

nes_emu/Effects_Buffer.cpp

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,13 @@ Effects_Buffer::Effects_Buffer( bool center_only ) : Multi_Buffer( 2 )
7373

7474
reverb_buf = NULL;
7575
reverb_pos = 0;
76-
76+
77+
extra_echo_buf = NULL;
78+
extra_reverb_buf = NULL;
79+
extra_echo_pos = 0;
80+
extra_reverb_pos = 0;
81+
extra_valid = false;
82+
7783
stereo_remain = 0;
7884
effect_remain = 0;
7985
effects_enabled = false;
@@ -84,6 +90,8 @@ Effects_Buffer::~Effects_Buffer()
8490
{
8591
delete [] echo_buf;
8692
delete [] reverb_buf;
93+
delete [] extra_echo_buf;
94+
delete [] extra_reverb_buf;
8795
}
8896

8997
const char *Effects_Buffer::set_sample_rate( long rate, int msec )
@@ -99,6 +107,20 @@ const char *Effects_Buffer::set_sample_rate( long rate, int msec )
99107
reverb_buf = new blip_sample_t [reverb_size];
100108
CHECK_ALLOC( reverb_buf );
101109
}
110+
111+
if ( !extra_echo_buf )
112+
{
113+
extra_echo_buf = new blip_sample_t [echo_size];
114+
CHECK_ALLOC( extra_echo_buf );
115+
memset( extra_echo_buf, 0, echo_size * sizeof *extra_echo_buf );
116+
}
117+
118+
if ( !extra_reverb_buf )
119+
{
120+
extra_reverb_buf = new blip_sample_t [reverb_size];
121+
CHECK_ALLOC( extra_reverb_buf );
122+
memset( extra_reverb_buf, 0, reverb_size * sizeof *extra_reverb_buf );
123+
}
102124

103125
for ( int i = 0; i < buf_count; i++ )
104126
RETURN_ERR( bufs [i].set_sample_rate( rate, msec ) );
@@ -513,3 +535,32 @@ void Effects_Buffer::mix_enhanced( blip_sample_t* out, long count )
513535
l2.end( bufs [5] );
514536
r2.end( bufs [6] );
515537
}
538+
539+
void Effects_Buffer::SaveAudioBufferState()
540+
{
541+
SaveAudioBufferStatePrivate();
542+
for ( int i = 0; i < buf_count; i++ )
543+
bufs [i].SaveAudioBufferState();
544+
extra_echo_pos = echo_pos;
545+
extra_reverb_pos = reverb_pos;
546+
if ( echo_buf && extra_echo_buf )
547+
memcpy( extra_echo_buf, echo_buf, echo_size * sizeof *echo_buf );
548+
if ( reverb_buf && extra_reverb_buf )
549+
memcpy( extra_reverb_buf, reverb_buf, reverb_size * sizeof *reverb_buf );
550+
extra_valid = true;
551+
}
552+
553+
void Effects_Buffer::RestoreAudioBufferState()
554+
{
555+
if ( !extra_valid )
556+
return; // freshly-constructed buffer; nothing snapshotted yet
557+
RestoreAudioBufferStatePrivate();
558+
for ( int i = 0; i < buf_count; i++ )
559+
bufs [i].RestoreAudioBufferState();
560+
echo_pos = extra_echo_pos;
561+
reverb_pos = extra_reverb_pos;
562+
if ( echo_buf && extra_echo_buf )
563+
memcpy( echo_buf, extra_echo_buf, echo_size * sizeof *echo_buf );
564+
if ( reverb_buf && extra_reverb_buf )
565+
memcpy( reverb_buf, extra_reverb_buf, reverb_size * sizeof *reverb_buf );
566+
}

nes_emu/Effects_Buffer.h

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,19 @@ class Effects_Buffer : public Multi_Buffer {
6868
blip_sample_t* echo_buf;
6969
int reverb_pos;
7070
int echo_pos;
71-
71+
72+
// Snapshot buffers for SaveAudioBufferState / RestoreAudioBufferState.
73+
// Allocated alongside echo_buf / reverb_buf in set_sample_rate, same
74+
// sizes. Effects_Buffer is the only Multi_Buffer subclass with internal
75+
// delay lines beyond the Blip_Buffers themselves, so it needs to
76+
// snapshot them or runahead audio with effects enabled corrupts after
77+
// every restore.
78+
blip_sample_t* extra_echo_buf;
79+
blip_sample_t* extra_reverb_buf;
80+
int extra_echo_pos;
81+
int extra_reverb_pos;
82+
bool extra_valid;
83+
7284
struct {
7385
fixed_t pan_1_levels [2];
7486
fixed_t pan_2_levels [2];
@@ -84,6 +96,9 @@ class Effects_Buffer : public Multi_Buffer {
8496
void mix_stereo( blip_sample_t*, long );
8597
void mix_enhanced( blip_sample_t*, long );
8698
void mix_mono_enhanced( blip_sample_t*, long );
99+
public:
100+
virtual void SaveAudioBufferState();
101+
virtual void RestoreAudioBufferState();
87102
};
88103

89104
inline Effects_Buffer::channel_t Effects_Buffer::channel( int i ) {

nes_emu/Nes_Buffer.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ Nes_Nonlinearizer::Nes_Nonlinearizer()
177177
}
178178
extra_accum = 0;
179179
extra_prev = 0;
180+
extra_valid = false;
180181
}
181182

182183
Nes_Apu* Nes_Nonlinearizer::enable( bool b, Blip_Buffer* buf )
@@ -231,10 +232,13 @@ void Nes_Nonlinearizer::SaveAudioBufferState()
231232
{
232233
extra_accum = accum;
233234
extra_prev = prev;
235+
extra_valid = true;
234236
}
235237

236238
void Nes_Nonlinearizer::RestoreAudioBufferState()
237239
{
240+
if ( !extra_valid )
241+
return;
238242
accum = extra_accum;
239243
prev = extra_prev;
240244
}

nes_emu/Nes_Buffer.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class Nes_Nonlinearizer {
2020

2121
long extra_accum;
2222
long extra_prev;
23+
bool extra_valid; // see Blip_Buffer.h
2324
public:
2425
Nes_Nonlinearizer();
2526
bool enabled;

nes_emu/Nes_Effects_Buffer.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,12 @@ long Nes_Effects_Buffer::read_samples( blip_sample_t* out, long count )
8484

8585
void Nes_Effects_Buffer::SaveAudioBufferState()
8686
{
87+
Effects_Buffer::SaveAudioBufferState();
88+
nonlin.SaveAudioBufferState();
8789
}
8890

8991
void Nes_Effects_Buffer::RestoreAudioBufferState()
9092
{
93+
Effects_Buffer::RestoreAudioBufferState();
94+
nonlin.RestoreAudioBufferState();
9195
}

0 commit comments

Comments
 (0)