maple: what a game saves is written to its file by a thread of its own

A game saves to a memory card in the middle of a frame; the bytes were
written to the card's file between two frames, by the thread that runs
them, which waited for the write and the flush. Next to nothing on a disk
with the system's cache in front of it; a frame or several where a write
goes through to a memory card, a network share or another process, and
the sound then stutters while a game saves. A NAOMI cabinet's EEPROM the
same, with the file made anew each time. (The audit's throughput item 6.)

core/savewriter.c: the bytes are copied and handed to a writer thread - a
list pushed onto with one compare-and-swap, the writer woken through an
eventcount; no lock, and no system call unless the writer is asleep - and
the writer does the waiting. Writes reach the file in the order they
were handed over. No more than 4 MB waits at once: past that the hand-over
is refused, the device keeps what it had to save and tries at the next
frame, by which time it is one write. A device waits for the writer
before it closes its file or goes; the thread ends with the content.
Without threads (TARGET_NO_THREADS) the write is made on the spot, as it
was.

And a write that fails is no longer forgotten. The flush used to count
what was to be written as written whatever seek, write and flush said, so
a card whose file could not be written for a moment stayed different from
its file for good. The writer tells the device, and the device writes the
whole card again (the EEPROM again) about a second later, for as long as
it goes on failing, and says so in the log.

tools/threads:
  - savewriter_test.c, run by run.sh under ThreadSanitizer and
    AddressSanitizer: the writer over a file system that can be made slow
    and made to fail. 2000 writes over one another leave the file as
    writing straight to it would; 40 writes of 5 ms each are handed over
    in 0.05 ms and written 206 ms later; a failed write sets the caller's
    flag and the card written again is in the file; more than may wait is
    refused and what was taken is written; stop and start.
  - the test disc's program writes a quarter of a block to the memory
    card in the controller, and headless.sh looks for it in the card's
    file after each of its runs (a new card each run).

The EEPROM files Virtua Fighter 4 and Dead or Alive 2 leave after 900
frames are the same files as before, with threaded rendering on and off.
What a failed write leads to in the devices has been run in the writer's
test, not in the core: the files here do not fail.
This commit is contained in:
LibretroAdmin authored and libretroadmin committed 2026-10-09 07:16:31 +02:00
1 parent bcdd1b5b72
commit 8f33290211
10 files changed
+1413 -724

No files matched your search

+1
View File
@@ -399,6 +399,7 @@ SOURCES_CXX += $(CORE_DIR)/core/libretro/libretro.cpp \
$(CORE_DIR)/core/libretro/vmem_utils.cpp
SOURCES_C += $(DEPS_DIR)/coreio/coreio.c
SOURCES_C += $(CORE_DIR)/core/savewriter.c
# When statically linked into RetroArch (e.g. libnx), the frontend already
# compiles these libretro-common sources; building them again would duplicate
+72 -14
View File
@@ -6,6 +6,7 @@
#include "maple_cfg.h"
#include "hw/pvr/spg.h"
#include "hw/naomi/naomi_cart.h"
#include "savewriter.h"
#include <math.h>
#include <time.h>
@@ -564,6 +565,34 @@ static bool vmu_inflate_default(u8 *dst, size_t dst_len)
return rv == RDEFLATE_PROCESS_END && wr == dst_len;
}
/* How the writes a device hands to the save writer are getting on. */
struct MapleSaveState
{
retro_atomic_int_t failed; // set by the writer: a write did not go through
u32 wait; // frames until the next try, after one that failed
MapleSaveState() : wait(0)
{
retro_atomic_int_init(&failed, 0);
}
// Whether a write has failed since this was last asked; a while before the next try
bool Failed()
{
if (!retro_atomic_load_acquire_int(&failed) || !retro_atomic_exchange_int(&failed, 0))
return false;
wait = 60;
return true;
}
// Whether it is still too soon after a failure to try again
bool Waiting()
{
if (wait == 0)
return false;
wait--;
return true;
}
};
struct maple_sega_vmu: maple_base
{
RFILE* file;
@@ -571,6 +600,7 @@ struct maple_sega_vmu: maple_base
u32 dirty_lo;
u32 dirty_hi;
bool full_save_needed = false;
MapleSaveState save_state; // of the writes handed over: see FlushSave()
u8 flash_data[128*1024];
u8 lcd_data[192];
u8 lcd_data_decoded[VMU_SCREEN_WIDTH*VMU_SCREEN_HEIGHT];
@@ -595,6 +625,7 @@ struct maple_sega_vmu: maple_base
* next thing the game writes cannot be patched into the file, which
* would leave a card that is part one and part the other - a broken
* file system. The whole card is written then. */
save_state.wait = 0;
FlushSave();
full_save_needed = true;
LIBRETRO_USA(flash_data,128*1024);
@@ -641,15 +672,31 @@ struct maple_sega_vmu: maple_base
NOTICE_LOG(MAPLE, "Loaded VMU from file \"%s\"", apath.c_str());
}
}
/* What the game wrote to the card since the last time goes to the
* card's file: handed to the writer's thread (savewriter.h), which
* does the waiting for the disk. The file is the writer's from the
* first hand-over on.
*
* A write that fails is found out later: the whole card is then
* written again, since which part of the file is not what it should
* be is no longer known, a second or so later and for as long as it
* goes on failing. It used to be forgotten - what was to be written
* counted as written whatever the write said. */
virtual void FlushSave()
{
if (dirty_lo >= dirty_hi)
if (save_state.Failed())
{
WARN_LOG(MAPLE, "VMU %s: a write to its file failed. The card will be written again", logical_port);
dirty_lo = 0;
dirty_hi = sizeof(flash_data);
}
if (dirty_lo >= dirty_hi || save_state.Waiting())
return;
if (file)
{
filestream_seek(file, dirty_lo, RETRO_VFS_SEEK_POSITION_START);
filestream_write(file, &flash_data[dirty_lo], dirty_hi - dirty_lo);
filestream_flush(file);
// (not taken: too much is waiting to be written. Next frame.)
if (!save_writer_put(file, dirty_lo, &flash_data[dirty_lo], dirty_hi - dirty_lo, &save_state.failed))
return;
}
else
INFO_LOG(MAPLE, "Failed to save VMU %s data", logical_port);
@@ -658,7 +705,12 @@ struct maple_sega_vmu: maple_base
}
virtual ~maple_sega_vmu()
{
save_state.wait = 0;
FlushSave();
// the writer has the file, and the flag that is this card's
save_writer_drain();
if (file && save_state.Failed())
WARN_LOG(MAPLE, "VMU %s: the last write to its file failed", logical_port);
if (file) filestream_close(file);
}
virtual u32 dma(u32 cmd)
@@ -2069,30 +2121,36 @@ struct maple_naomi_jamma : maple_sega_controller
u8 jvs_receive_buffer[32][258];
u32 jvs_receive_length[32] = { 0 };
bool eeprom_dirty = false;
MapleSaveState save_state;
maple_naomi_jamma()
{
}
virtual ~maple_naomi_jamma()
{
save_state.wait = 0;
FlushSave();
// (the flag is this device's: the writer is done with it first)
save_writer_drain();
if (save_state.Failed())
WARN_LOG(MAPLE, "Cannot save EEPROM to file %s", eeprom_file);
EEPROM_loaded = false;
}
/* The EEPROM, when the game has written to it, to its file: made
* anew each time, by the writer's thread (savewriter.h). Tried again
* later if that fails, as a memory card's is. */
virtual void FlushSave()
{
RFILE* f;
if (!eeprom_dirty)
return;
eeprom_dirty = false;
f = filestream_open(eeprom_file, RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE);
if (f)
if (save_state.Failed())
{
filestream_write(f, EEPROM, 0x80);
filestream_close(f);
WARN_LOG(MAPLE, "Cannot save EEPROM to file %s. It will be tried again", eeprom_file);
eeprom_dirty = true;
}
else
WARN_LOG(MAPLE, "Cannot save EEPROM to file %s", eeprom_file);
if (!eeprom_dirty || save_state.Waiting())
return;
if (save_writer_put_file(eeprom_file, EEPROM, 0x80, &save_state.failed))
eeprom_dirty = false;
}
void create_io_boards()
+4
View File
@@ -56,6 +56,7 @@ char* strdup(const char *str)
#include "hw/maple/maple_cfg.h"
#include "../hw/pvr/spg.h"
#include "../hw/naomi/naomi_cart.h"
#include "../savewriter.h"
#include "../archive/archive.h"
#include "../imgread/common.h"
#include "../hw/aica/dsp.h"
@@ -2442,6 +2443,9 @@ void retro_unload_game(void)
dc_stop();
dc_term();
}
/* The devices have handed over the last of what they had to save and
* waited for it to be written: the writer's thread goes too. */
save_writer_stop();
}
+253
View File
@@ -0,0 +1,253 @@
/* See savewriter.h. */
#include "savewriter.h"
#include <stdlib.h>
#include <string.h>
#ifndef TARGET_NO_THREADS
#include <rthreads/rthreads.h>
#include <rthreads/retro_eventcount.h>
#endif
/* Never more than this waiting to be written: eight memory cards written
* whole are a quarter of it. */
#define SAVE_WRITER_MAX_WAITING (4u * 1024u * 1024u)
typedef struct save_job
{
struct save_job *next;
RFILE *file; /* NULL: the file at path, made anew */
int64_t offset;
size_t len;
retro_atomic_int_t *failed;
/* then the path with its 0, if there is one, and the bytes */
size_t path_size;
} save_job_t;
#define SAVE_JOB_PATH(job) ((char *)((job) + 1))
#define SAVE_JOB_DATA(job) ((uint8_t *)((job) + 1) + (job)->path_size)
static int save_job_run(const save_job_t *job)
{
int ok = 1;
if (job->file)
{
if (filestream_seek(job->file, job->offset, RETRO_VFS_SEEK_POSITION_START) < 0)
return 0;
if (filestream_write(job->file, SAVE_JOB_DATA(job), (int64_t)job->len) != (int64_t)job->len)
ok = 0;
if (filestream_flush(job->file) != 0)
ok = 0;
}
else
{
RFILE *f = filestream_open(SAVE_JOB_PATH(job), RETRO_VFS_FILE_ACCESS_WRITE,
RETRO_VFS_FILE_ACCESS_HINT_NONE);
if (!f)
return 0;
if (filestream_write(f, SAVE_JOB_DATA(job), (int64_t)job->len) != (int64_t)job->len)
ok = 0;
if (filestream_close(f) != 0)
ok = 0;
}
return ok;
}
static save_job_t *save_job_new(RFILE *file, const char *path, int64_t offset,
const void *data, size_t len, retro_atomic_int_t *failed)
{
const size_t path_size = path ? strlen(path) + 1 : 0;
save_job_t *job = (save_job_t *)malloc(sizeof(*job) + path_size + len);
if (!job)
return NULL;
job->next = NULL;
job->file = file;
job->offset = offset;
job->len = len;
job->failed = failed;
job->path_size = path_size;
if (path)
memcpy(SAVE_JOB_PATH(job), path, path_size);
memcpy(SAVE_JOB_DATA(job), data, len);
return job;
}
#ifdef TARGET_NO_THREADS
static int save_writer_take(save_job_t *job)
{
if (!job)
return 0;
if (!save_job_run(job) && job->failed)
retro_atomic_store_release_int(job->failed, 1);
free(job);
return 1;
}
void save_writer_drain(void) { }
void save_writer_stop(void) { }
#else
static struct
{
sthread_t *thread;
retro_eventcount_t work; /* the writer sleeps here */
retro_eventcount_t idle; /* and who drains, here */
retro_atomic_ptr_t head; /* what was handed over, newest first */
retro_atomic_size_t waiting; /* bytes handed over and not yet written */
retro_atomic_int_t stop;
int ready; /* the two above it are made */
} writer;
static void save_writer_thread(void *unused)
{
(void)unused;
for (;;)
{
save_job_t *list = (save_job_t *)retro_atomic_exchange_ptr(&writer.head, NULL);
save_job_t *job = NULL;
if (!list)
{
int key;
if (retro_atomic_load_acquire_int(&writer.stop))
break;
key = retro_eventcount_prepare_wait(&writer.work);
if (retro_atomic_load_acquire_ptr(&writer.head) != NULL
|| retro_atomic_load_acquire_int(&writer.stop))
retro_eventcount_cancel_wait(&writer.work);
else
retro_eventcount_commit_wait(&writer.work, key);
continue;
}
/* oldest first: a later write to the same bytes has to land last */
while (list)
{
save_job_t *next = list->next;
list->next = job;
job = list;
list = next;
}
while (job)
{
save_job_t *next = job->next;
if (!save_job_run(job) && job->failed)
retro_atomic_store_release_int(job->failed, 1);
retro_atomic_fetch_sub_size(&writer.waiting, sizeof(*job) + job->path_size + job->len);
free(job);
job = next;
}
retro_eventcount_notify(&writer.idle);
}
}
static int save_writer_start(void)
{
if (writer.thread)
return 1;
if (!writer.ready)
{
if (!retro_eventcount_init(&writer.work))
return 0;
if (!retro_eventcount_init(&writer.idle))
{
retro_eventcount_free(&writer.work);
return 0;
}
retro_atomic_ptr_init(&writer.head, NULL);
retro_atomic_size_init(&writer.waiting, 0);
writer.ready = 1;
}
retro_atomic_int_init(&writer.stop, 0);
writer.thread = sthread_create(save_writer_thread, NULL);
return writer.thread != NULL;
}
static int save_writer_take(save_job_t *job)
{
const size_t size = job ? sizeof(*job) + job->path_size + job->len : 0;
if (!job)
return 0;
if (!save_writer_start())
{
/* no thread to be had: written here, as without threads */
if (!save_job_run(job) && job->failed)
retro_atomic_store_release_int(job->failed, 1);
free(job);
return 1;
}
if (retro_atomic_load_acquire_size(&writer.waiting) + size > SAVE_WRITER_MAX_WAITING)
{
free(job);
return 0;
}
retro_atomic_fetch_add_size(&writer.waiting, size);
for (;;)
{
void *old = retro_atomic_load_relaxed_ptr(&writer.head);
job->next = (save_job_t *)old;
if (retro_atomic_cas_ptr(&writer.head, old, job))
break;
}
retro_eventcount_notify(&writer.work);
return 1;
}
void save_writer_drain(void)
{
if (!writer.thread)
return;
for (;;)
{
int key;
if (retro_atomic_load_acquire_size(&writer.waiting) == 0)
return;
key = retro_eventcount_prepare_wait(&writer.idle);
if (retro_atomic_load_acquire_size(&writer.waiting) == 0)
{
retro_eventcount_cancel_wait(&writer.idle);
return;
}
retro_eventcount_commit_wait(&writer.idle, key);
}
}
void save_writer_stop(void)
{
if (!writer.thread)
return;
save_writer_drain();
retro_atomic_store_release_int(&writer.stop, 1);
retro_eventcount_notify(&writer.work);
sthread_join(writer.thread);
writer.thread = NULL;
}
#endif
int save_writer_put(RFILE *file, int64_t offset, const void *data,
size_t len, retro_atomic_int_t *failed)
{
if (!file)
return 0;
return save_writer_take(save_job_new(file, NULL, offset, data, len, failed));
}
int save_writer_put_file(const char *path, const void *data, size_t len,
retro_atomic_int_t *failed)
{
if (!path)
return 0;
return save_writer_take(save_job_new(NULL, path, 0, data, len, failed));
}
+62
View File
@@ -0,0 +1,62 @@
/* What the machine saves - a memory card's blocks, a cabinet's EEPROM -
* goes to its file from a thread of its own.
*
* A game saves in the middle of a frame; the bytes used to be written to
* the file between two frames, by the thread that runs them, which then
* waited for the write: nothing on a disk with its cache in front of it,
* a frame or several where a write goes through to a memory card or to
* another process. Here the bytes are copied and handed over, which takes
* no lock and makes no system call unless the writer is asleep, and the
* writer does the waiting.
*
* Whoever saves is one thread at a time: the one that runs the frames, or
* the emulation thread while that one waits for it. The hand-over would
* take more than one at once (it is a list any thread may push onto), the
* writer's start would not.
*
* A write can fail, and is found to have failed after the caller has gone
* on. The caller gives a flag for that, which the writer sets; what the
* caller does when it finds it set - write everything again, later - is
* its own business. Until now a write that failed was forgotten.
*
* Built without threads (TARGET_NO_THREADS) the calls write before they
* return, and report through the same flag. */
#ifndef CORE_SAVEWRITER_H_
#define CORE_SAVEWRITER_H_
#include <stddef.h>
#include <stdint.h>
#include <retro_common_api.h>
#include <retro_atomic.h>
#include <streams/file_stream.h>
RETRO_BEGIN_DECLS
/* @len bytes of @data, copied, to be written at @offset of @file and the
* file flushed. @file is open for writing and is the writer's from here
* until save_writer_drain() has returned: nobody else seeks, reads or
* closes it in between.
*
* Returns 0 if they were not taken - no memory, or too much is waiting
* already (the writer cannot keep up) - and are still the caller's to
* save; 1 if they were. @failed is set to 1 if the write then fails; it
* has to outlive the write, which it does if the caller drains before it
* goes. */
int save_writer_put(RFILE *file, int64_t offset, const void *data,
size_t len, retro_atomic_int_t *failed);
/* The same for a file that is made anew each time: @len bytes of @data to
* be the whole of the file at @path. */
int save_writer_put_file(const char *path, const void *data, size_t len,
retro_atomic_int_t *failed);
/* Returns when everything handed over has been written, or has failed. */
void save_writer_drain(void);
/* The same, and the writer's thread is ended: at the end of the content.
* The next hand-over starts it again. */
void save_writer_stop(void);
RETRO_END_DECLS
#endif
+10
View File
@@ -213,6 +213,8 @@ for RUN_AS in legacy accurate "legacy wince" "legacy reset" "legacy swap" "legac
fi
SOUND=$WORK/sound.pcm
[ "$NAME" != legacy ] || SOUND=$WORK/sound-plain.pcm
# (the memory card the program writes to: a new one each run, see below)
rm -f "$WORK/dir/dc/vmu_save_A1.bin"
HEADLESS_DIR=$WORK/dir HEADLESS_SOUND=$SOUND HEADLESS_PEEK=$VERDICT \
$RUN "$WORK/$FRONTEND" "$CORE" "$WORK/test.gdi" $FRAMES \
reicast_use_real_bios=disabled reicast_threaded_rendering=disabled \
@@ -251,6 +253,14 @@ for RUN_AS in legacy accurate "legacy wince" "legacy reset" "legacy swap" "legac
echo "FAIL: the disc's program found something wrong: $(cat "$WORK/$NAME.out")" >&2
exit 1
}
# The program wrote to the memory card in the controller, and the card
# said it had it. The core writes that to the card's file from a thread
# of its own (core/savewriter.c), not while the machine waits: it has to
# be in the file all the same.
python3 "$T/live_disc.py" --card "$WORK/dir/dc/vmu_save_A1.bin" || {
echo "FAIL: what the program wrote to the memory card is not in the card's file" >&2
exit 1
}
# (the sound of a run with a reset or a disc change in it is not the plain one: not looked at)
if [ -n "$HEADLESS_SAVE" ]; then
python3 "$T/live_audio.py" --reloaded "$WORK/sound-plain.pcm" "$SOUND" || {
+741 -710
View File
File diff suppressed because it is too large. Load diff
+27
View File
@@ -194,6 +194,33 @@ static u32 maple_test(void)
if (reply[29] != ('V' | ('e' << 8) | ('r' << 16) | ('s' << 24)))
return 5;
}
/* The memory card in the controller's first slot is written to: a
* quarter of a block (a card takes a block in four parts), block 100,
* which is the middle of where a game's files go. The card says it has
* them. Whether they then reach the card's file is for whoever runs
* this to look at (headless.sh): the core writes them out behind the
* machine's back, from a thread of its own. With no card in the slot
* nobody answers, and there is nothing to tell by. */
{
volatile u32 *frame = (volatile u32 *)0xAC00E000;
u32 i;
reply[0] = 0x12345678;
frame[0] = 0x80000000 | 34; /* last frame, port A, 35 words */
frame[1] = 0x0C00E100;
frame[2] = 0x0C | (0x01 << 8) | (34 << 24); /* block write, to A1 */
frame[3] = 0x02000000; /* function: storage */
frame[4] = 100u << 24; /* partition 0, part 0, block 100 */
for (i = 0; i < 32; i++)
frame[5 + i] = 0xC0DE0000 + i;
SB(0xC04) = 0x0C00E000;
SB(0xC18) = 1;
for (guard = 0; guard < 2000000 && (SB(0xC18) & 1); guard++)
;
if (reply[0] != 0xFFFFFFFF && (reply[0] & 0xFF) != 7)
return 6;
}
return 0;
}
+11
View File
@@ -81,8 +81,19 @@ for san in thread address,undefined; do
-o "$WORK/pico_test" tools/threads/pico_test.cpp core/network/picoppp.cpp \
core/hw/modem/dns.cpp "$WORK"/pico/*.o "$WORK/rthreads.o" "$WORK/features_cpu.o" \
"$WORK/rtime.o" -lpthread
# The save writer: the core's own savewriter.c over libretro-common's
# file streams, with a file system of the test's that can be slow and
# can fail.
$CC -O1 -g -fsanitize=$san -fno-sanitize-recover=undefined $INC \
-o "$WORK/savewriter_test" tools/threads/savewriter_test.c core/savewriter.c \
$L/streams/file_stream.c $L/vfs/vfs_implementation.c $L/file/file_path.c \
$L/file/file_path_io.c $L/compat/compat_strl.c $L/compat/fopen_utf8.c \
$L/compat/compat_strcasestr.c $L/encodings/encoding_utf.c $L/string/stdstring.c \
$L/string/rstrtod.c "$WORK/rthreads.o" "$WORK/retro_eventcount.o" \
"$WORK/features_cpu.o" "$WORK/rtime.o" -lpthread -lm
echo "== -fsanitize=$san"
TSAN_OPTIONS=halt_on_error=1 "$WORK/threads_test"
TSAN_OPTIONS=halt_on_error=1 "$WORK/savewriter_test" "$WORK"
TSAN_OPTIONS=halt_on_error=1 "$WORK/pico_test"
TSAN_OPTIONS=halt_on_error=1 "$WORK/pico_test" bba
TSAN_OPTIONS=halt_on_error=1 "$WORK/m3comm_test"
+232
View File
@@ -0,0 +1,232 @@
/* The save writer (core/savewriter.c) against a file system that can be
* made slow and made to fail: the libretro-common one behind a VFS
* interface of the test's, as a frontend's would be.
*
* - writes handed over in any number land in the file as they would
* have written straight to it, the later over the earlier;
* - handing over does not wait for the disk: with a write that takes
* five milliseconds, forty of them are handed over in a fraction of
* the time they take to be written;
* - a write that fails sets the caller's flag, and what is written
* again afterwards is in the file;
* - a file made anew each time is what was last handed over, and no
* longer than that;
* - more than the writer may have waiting is refused, and nothing that
* was taken is lost;
* - the writer can be stopped and started again.
*
* tools/threads/run.sh builds and runs it, under ThreadSanitizer and
* under AddressSanitizer. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libretro.h>
#include <retro_timers.h>
#include <features/features_cpu.h>
#include <streams/file_stream.h>
#include <vfs/vfs_implementation.h>
#include "savewriter.h"
#define CARD_SIZE (128 * 1024)
static retro_atomic_int_t write_delay_ms; /* each write takes this long */
static retro_atomic_int_t writes_to_fail; /* the next so many fail */
static retro_atomic_int_t writes_seen;
static int64_t RETRO_CALLCONV slow_write(struct retro_vfs_file_handle *stream, const void *s, uint64_t len)
{
const int delay = retro_atomic_load_acquire_int(&write_delay_ms);
retro_atomic_fetch_add_int(&writes_seen, 1);
if (delay > 0)
retro_sleep((unsigned)delay);
if (retro_atomic_load_acquire_int(&writes_to_fail) > 0)
{
retro_atomic_fetch_sub_int(&writes_to_fail, 1);
return -1;
}
return retro_vfs_file_write_impl((libretro_vfs_implementation_file *)stream, s, len);
}
static struct retro_vfs_interface vfs =
{
(retro_vfs_get_path_t)retro_vfs_file_get_path_impl,
(retro_vfs_open_t)retro_vfs_file_open_impl,
(retro_vfs_close_t)retro_vfs_file_close_impl,
(retro_vfs_size_t)retro_vfs_file_size_impl,
(retro_vfs_tell_t)retro_vfs_file_tell_impl,
(retro_vfs_seek_t)retro_vfs_file_seek_impl,
(retro_vfs_read_t)retro_vfs_file_read_impl,
slow_write,
(retro_vfs_flush_t)retro_vfs_file_flush_impl,
(retro_vfs_remove_t)retro_vfs_file_remove_impl,
(retro_vfs_rename_t)retro_vfs_file_rename_impl,
(retro_vfs_truncate_t)retro_vfs_file_truncate_impl,
};
static int failures;
static void check(int ok, const char *what)
{
if (!ok)
{
fprintf(stderr, "FAIL: %s\n", what);
failures++;
}
}
static unsigned rnd_state = 12345;
static unsigned rnd(void)
{
rnd_state = rnd_state * 1103515245u + 12345u;
return (rnd_state >> 8) & 0xFFFFFF;
}
/* The file's bytes; its length to @len. */
static uint8_t *slurp(const char *path, int64_t *len)
{
void *buf = NULL;
*len = 0;
if (!filestream_read_file(path, &buf, len))
return NULL;
return (uint8_t *)buf;
}
int main(int argc, char **argv)
{
static uint8_t card[CARD_SIZE], chunk[CARD_SIZE];
struct retro_vfs_interface_info info;
char card_path[1024], small_path[1024];
retro_atomic_int_t failed;
const char *dir = argc > 1 ? argv[1] : ".";
uint8_t *got;
int64_t got_len;
RFILE *file;
int i, taken, refused;
retro_time_t t0, t_put, t_all;
retro_atomic_int_init(&write_delay_ms, 0);
retro_atomic_int_init(&writes_to_fail, 0);
retro_atomic_int_init(&writes_seen, 0);
retro_atomic_int_init(&failed, 0);
info.required_interface_version = 2;
info.iface = &vfs;
filestream_vfs_init(&info);
snprintf(card_path, sizeof(card_path), "%s/savewriter_card.bin", dir);
snprintf(small_path, sizeof(small_path), "%s/savewriter_small.bin", dir);
/* a card's file, as the core makes one: written whole, then patched */
memset(card, 0xE5, sizeof(card));
file = filestream_open(card_path, RETRO_VFS_FILE_ACCESS_READ_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE);
check(file != NULL, "the card's file is made");
if (!file)
return 1;
check(filestream_write(file, card, sizeof(card)) == (int64_t)sizeof(card), "the card's file is filled");
/* many writes, some over others */
for (i = 0; i < 2000; i++)
{
const unsigned at = rnd() % (CARD_SIZE - 512);
const unsigned len = 1 + rnd() % 512;
unsigned k;
for (k = 0; k < len; k++)
card[at + k] = (uint8_t)rnd();
check(save_writer_put(file, at, card + at, len, &failed), "a write is taken");
}
save_writer_drain();
check(!retro_atomic_load_acquire_int(&failed), "none of them failed");
got = slurp(card_path, &got_len);
check(got && got_len == CARD_SIZE && !memcmp(got, card, CARD_SIZE), "the file is what was written, the later over the earlier");
free(got);
/* a slow disk: handing over does not wait for it */
retro_atomic_store_release_int(&write_delay_ms, 5);
t0 = cpu_features_get_time_usec();
for (i = 0; i < 40; i++)
{
const unsigned at = (unsigned)i * 512;
memset(card + at, i + 1, 512);
check(save_writer_put(file, at, card + at, 512, &failed), "a write to the slow disk is taken");
}
t_put = cpu_features_get_time_usec() - t0;
save_writer_drain();
t_all = cpu_features_get_time_usec() - t0;
retro_atomic_store_release_int(&write_delay_ms, 0);
printf("slow disk: 40 writes of 5 ms handed over in %.2f ms, written after %.0f ms\n",
t_put / 1000.0, t_all / 1000.0);
check(t_all >= 40 * 5000 * 9 / 10, "the writes took the time they take");
check(t_put < t_all / 4, "handing them over did not wait for the disk");
got = slurp(card_path, &got_len);
check(got && got_len == CARD_SIZE && !memcmp(got, card, CARD_SIZE), "the file is what was written to the slow disk");
free(got);
/* a write that fails */
retro_atomic_store_release_int(&writes_to_fail, 1);
memset(card + 4096, 0x5A, 512);
check(save_writer_put(file, 4096, card + 4096, 512, &failed), "the write that will fail is taken");
save_writer_drain();
check(retro_atomic_load_acquire_int(&failed) == 1, "the caller is told of the write that failed");
retro_atomic_store_release_int(&failed, 0);
/* what the core then does: the whole card again */
check(save_writer_put(file, 0, card, CARD_SIZE, &failed), "the whole card is taken");
save_writer_drain();
check(!retro_atomic_load_acquire_int(&failed), "written again, it did not fail");
got = slurp(card_path, &got_len);
check(got && got_len == CARD_SIZE && !memcmp(got, card, CARD_SIZE), "the file is the card after the failure");
free(got);
/* more than may wait is refused; what was taken is written */
retro_atomic_store_release_int(&write_delay_ms, 2);
taken = refused = 0;
for (i = 0; i < 64; i++)
{
memset(chunk, 0x80 + i, sizeof(chunk));
if (save_writer_put(file, 0, chunk, sizeof(chunk), &failed))
{
memcpy(card, chunk, sizeof(chunk));
taken++;
}
else
refused++;
}
save_writer_drain();
retro_atomic_store_release_int(&write_delay_ms, 0);
printf("too much at once: %d taken, %d refused\n", taken, refused);
check(taken >= 16 && refused > 0, "more than may wait was refused");
got = slurp(card_path, &got_len);
check(got && got_len == CARD_SIZE && !memcmp(got, card, CARD_SIZE), "the file is the last card that was taken");
free(got);
/* a file made anew each time; the writer stopped and started between */
check(save_writer_put_file(small_path, "0123456789abcdef", 16, &failed), "a whole file is taken");
save_writer_stop();
got = slurp(small_path, &got_len);
check(got && got_len == 16 && !memcmp(got, "0123456789abcdef", 16), "the whole file is there once the writer is stopped");
free(got);
check(save_writer_put_file(small_path, "EEPROM", 6, &failed), "a whole file is taken by a writer started again");
save_writer_drain();
got = slurp(small_path, &got_len);
check(got && got_len == 6 && !memcmp(got, "EEPROM", 6), "the file is the shorter one that was last handed over");
free(got);
/* where there is nowhere to write it */
check(save_writer_put_file("/nonexistent-directory/savewriter.bin", "x", 1, &failed), "a file that cannot be made is taken");
save_writer_drain();
check(retro_atomic_load_acquire_int(&failed) == 1, "the caller is told the file could not be made");
save_writer_stop();
filestream_close(file);
filestream_delete(card_path);
filestream_delete(small_path);
if (failures)
return 1;
printf("save writer test passed (%d writes reached the file system)\n",
retro_atomic_load_acquire_int(&writes_seen));
return 0;
}