diff --git a/.github/workflows/chd.yml b/.github/workflows/chd.yml new file mode 100644 index 00000000..fc8ef66e --- /dev/null +++ b/.github/workflows/chd.yml @@ -0,0 +1,50 @@ +name: CHD reading + +# tools/chd/run.sh: every frame of CHDs made with each CD codec, a GD-ROM +# image and a parent chain against the files they came from; and CD audio +# and data read at once through chd.cpp, where the read-ahead threads must +# decode the hunks and neither stream throw out the other's (ASan + UBSan, +# TSan). + +on: + push: + paths: + - 'core/imgread/**' + - 'core/deps/coreio/**' + - 'core/archive/**' + - 'core/libretro-common/formats/**' + - 'core/libretro-common/encodings/**' + - 'core/libretro-common/vfs/**' + - 'core/libretro-common/streams/**' + - 'core/libretro-common/rthreads/**' + - 'tools/chd/**' + - '.github/workflows/chd.yml' + pull_request: + paths: + - 'core/imgread/**' + - 'core/deps/coreio/**' + - 'core/archive/**' + - 'core/libretro-common/formats/**' + - 'core/libretro-common/encodings/**' + - 'core/libretro-common/vfs/**' + - 'core/libretro-common/streams/**' + - 'core/libretro-common/rthreads/**' + - 'tools/chd/**' + - '.github/workflows/chd.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + chd: + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + - name: Install chdman + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends build-essential mame-tools python3 + - name: CHD reads, and CD audio and data at once + run: sh tools/chd/run.sh diff --git a/Makefile b/Makefile index a68d1cbb..c0e3ae32 100644 --- a/Makefile +++ b/Makefile @@ -1168,6 +1168,7 @@ endif ifeq ($(NO_THREADS),1) CORE_DEFINES += -DTARGET_NO_THREADS else + CORE_DEFINES += -DHAVE_THREADS NEED_PTHREAD=1 endif diff --git a/Makefile.common b/Makefile.common index e6e67ebd..508423c4 100644 --- a/Makefile.common +++ b/Makefile.common @@ -438,6 +438,7 @@ SOURCES_C += $(LIBRETRO_COMM_DIR)/memmap/memalign.c \ $(LIBRETRO_COMM_DIR)/file/file_path.c \ $(LIBRETRO_COMM_DIR)/file/file_path_io.c \ $(LIBRETRO_COMM_DIR)/vfs/vfs_implementation.c \ + $(LIBRETRO_COMM_DIR)/vfs/vfs_prefetch.c \ $(LIBRETRO_COMM_DIR)/streams/file_stream.c \ $(LIBRETRO_COMM_DIR)/streams/file_stream_transforms.c \ $(LIBRETRO_COMM_DIR)/encodings/encoding_utf.c \ diff --git a/core/imgread/chd.cpp b/core/imgread/chd.cpp index f2a79539..622ce571 100644 --- a/core/imgread/chd.cpp +++ b/core/imgread/chd.cpp @@ -1,11 +1,21 @@ #include "common.h" #include "chd_image.h" -#include "hunk_prefetch.h" + +#include /* tracks are padded to a multiple of this many frames */ const uint32_t CD_TRACK_PADDING = 4; +/* Which read-ahead a track reads through. CD audio and data are read + * from different places on the disc at once (music playing while a + * game streams), so each has its own, and neither throws out the + * hunks the other is about to use. */ +enum { CHD_STREAM_DATA, CHD_STREAM_AUDIO, CHD_STREAMS }; + +/* Hunks each stream's thread decodes ahead of its reader. */ +static const unsigned chd_stream_slots[CHD_STREAMS] = { 8, 4 }; + struct CHDDisc : Disc { chd_image_t* chd; @@ -14,16 +24,30 @@ struct CHDDisc : Disc u32 hunkbytes; u32 sph; + u32 total_hunks = 0; - /* Read-ahead on a worker thread, with a handle of its own. */ - chd_image_t* chd2 = nullptr; - HunkPrefetch prefetch; - static const u32 NO_HUNK = HunkPrefetch::NO_HUNK; - u32 total_hunks = 0; - - static bool prefetch_read(void *opaque, u32 hunk, u8 *dst) + /* A stream's read-ahead: libretro-common's prefetch thread, which + * decodes the hunks ahead of the reader on an image handle of its + * own. One window is one hunk. */ + struct Stream { - return chd_image_read_hunk(((CHDDisc *)opaque)->chd2, hunk, dst); + CHDDisc* disc; + chd_image_t* chd; + struct vfs_prefetch* p; + } streams[CHD_STREAMS]; + + static const u32 NO_HUNK = 0xFFFFFFFFu; + + static int64_t stream_fetch(void *user, uint64_t off, uint8_t *buf, size_t len) + { + Stream *st = (Stream *)user; + u32 hunk = (u32)(off / st->disc->hunkbytes); + + if (hunk >= st->disc->total_hunks) + return 0; + if (len < st->disc->hunkbytes || !chd_image_read_hunk(st->chd, hunk, buf)) + return -1; + return (int64_t)st->disc->hunkbytes; } /* Decompress @hunk into hunk_mem on this thread. A hunk that fails to @@ -40,66 +64,75 @@ struct CHDDisc : Disc } } - /* Return a pointer to the decompressed hunk, preferring the prefetched copy. */ - u8* hunk_get(u32 hunk) + /* Where the sector at @fad_offs is decoded: in the stream's window + * when its thread got there first, else decoded here. Valid until + * the next read on this disc. */ + const u8* sector_get(unsigned stream, u32 fad_offs) { - if (hunk == old_hunk) - return hunk_mem; /* still the current hunk */ - if (prefetch.Running()) - { - /* Takes the prefetched hunk if there is one and aims the worker - * at the next; on a miss (first read or seek) the worker gets - * going on the next hunk while this thread decodes this one. */ - bool hit = prefetch.Fetch(hunk); - hunk_mem = prefetch.Buffer(); - old_hunk = hit ? hunk : NO_HUNK; - if (hit) - return hunk_mem; - } - hunk_read_sync(hunk); - return hunk_mem; + const u32 hunk = fad_offs / sph; + const u32 in = (fad_offs % sph) * (2352 + 96); + const u8* ptr; + + if (streams[stream].p + && vfs_prefetch_view(streams[stream].p, (uint64_t)hunk * hunkbytes + in, &ptr) > 0) + return ptr; + if (hunk != old_hunk) + hunk_read_sync(hunk); + return hunk_mem + in; } - void start_prefetch(const char* file) + /* The stream's thread starts on @hunk now, ahead of the reads. */ + void stream_aim(unsigned stream, u32 hunk) { - chd2 = chd_image_open(file, NULL, 0); - if (chd2 == nullptr) - return; - if (!prefetch.Start(prefetch_read, this, hunkbytes, total_hunks)) - { - chd_image_close(chd2); - chd2 = nullptr; - return; - } - /* The prefetcher's buffers take over from the one TryOpen made. */ - delete [] hunk_mem; - hunk_mem = prefetch.Buffer(); - old_hunk = NO_HUNK; + if (streams[stream].p) + vfs_prefetch_seek(streams[stream].p, (uint64_t)hunk * hunkbytes); } - void stop_prefetch() + void start_streams(const char* file) { - if (prefetch.Running()) + for (unsigned i = 0; i < CHD_STREAMS; i++) { - prefetch.Stop(); - hunk_mem = nullptr; - old_hunk = NO_HUNK; + Stream& st = streams[i]; + st.disc = this; + if (!(st.chd = chd_image_open(file, NULL, 0))) + continue; + if (!(st.p = vfs_prefetch_new(stream_fetch, &st, hunkbytes, + chd_stream_slots[i], 0))) + { + chd_image_close(st.chd); + st.chd = nullptr; + } + } + } + + void stop_streams() + { + for (unsigned i = 0; i < CHD_STREAMS; i++) + { + vfs_prefetch_free(streams[i].p); + streams[i].p = nullptr; + chd_image_close(streams[i].chd); + streams[i].chd = nullptr; } - chd_image_close(chd2); - chd2 = nullptr; } CHDDisc() { chd=0; hunk_mem=0; + for (unsigned i = 0; i < CHD_STREAMS; i++) + { + streams[i].disc = this; + streams[i].chd = nullptr; + streams[i].p = nullptr; + } } bool TryOpen(const char* file); ~CHDDisc() { - stop_prefetch(); + stop_streams(); if (hunk_mem) delete [] hunk_mem; chd_image_close(chd); @@ -112,31 +145,28 @@ struct CHDTrack : TrackFile u32 StartFAD; u32 Offset; u32 fmt; + unsigned stream; bool swap_bytes; - CHDTrack(CHDDisc* disc, u32 StartFAD,u32 Offset, u32 fmt, bool swap_bytes) + CHDTrack(CHDDisc* disc, u32 StartFAD,u32 Offset, u32 fmt, unsigned stream, bool swap_bytes) { this->disc=disc; this->StartFAD=StartFAD; this->Offset=Offset; this->fmt=fmt; + this->stream=stream; this->swap_bytes = swap_bytes; } virtual void Read(u32 FAD,u8* dst,SectorFormat* sector_type,u8* subcode,SubcodeFormat* subcode_type) { - u32 fad_offs = FAD + Offset; - u32 hunk=(fad_offs)/disc->sph; - u8* hmem = disc->hunk_get(hunk); - - u32 hunk_ofs=fad_offs%disc->sph; + const u8* from = disc->sector_get(stream, FAD + Offset); if (swap_bytes) { /* The other byte order, made on the way across: 16 bits at a * time, which the compiler does sixteen bytes at a time. (It * was copied, and then swapped where it lay a byte at a time.) */ - const u8 *from = hmem + hunk_ofs * (2352 + 96); for (u32 i = 0; i + 1 < fmt; i += 2) { u16 w; @@ -146,7 +176,7 @@ struct CHDTrack : TrackFile } } else - memcpy(dst,hmem+hunk_ofs*(2352+96),fmt); + memcpy(dst,from,fmt); *sector_type=Format(); @@ -162,29 +192,28 @@ struct CHDTrack : TrackFile } /* The compressed bytes of every hunk the sectors span, so neither - * this thread nor the read-ahead worker stalls on a cold page. Both - * handles are on the same file, so one hint serves both. */ + * this thread nor the read-ahead stalls on a cold page (all handles + * are on the same file, so one hint serves them all), and the + * stream's thread set decoding from the first one. */ virtual void Prefetch(u32 FAD, u32 count) { u32 first = (FAD + Offset) / disc->sph; u32 last = (FAD + Offset + count - 1) / disc->sph; - if (count) - chd_image_prefetch(disc->chd, first, last - first + 1); + if (!count) + return; + chd_image_prefetch(disc->chd, first, last - first + 1); + disc->stream_aim(stream, first); } /* The sector inside the decoded hunk; a track whose bytes have to * be swapped is read through a buffer. */ virtual const u8* View(u32 FAD, SectorFormat* sector_type) { - u32 fad_offs = FAD + Offset; - u8* hmem; - if (swap_bytes) return NULL; - hmem = disc->hunk_get(fad_offs / disc->sph); *sector_type = Format(); - return hmem + (fad_offs % disc->sph) * (2352 + 96); + return disc->sector_get(stream, FAD + Offset); } }; @@ -275,7 +304,9 @@ bool CHDDisc::TryOpen(const char* file) last_pregap = pregap; /* Music is kept in the other byte order in a version 5 image (and * in any of a CD, which is read from version 5 only). */ - t.file = new CHDTrack(this, t.StartFAD, Offset + pregap_kept - t.StartFAD, sector_size, t.CTRL == 0 && chd_image_version(chd) >= 5); + t.file = new CHDTrack(this, t.StartFAD, Offset + pregap_kept - t.StartFAD, sector_size, + t.CTRL == 0 ? CHD_STREAM_AUDIO : CHD_STREAM_DATA, + t.CTRL == 0 && chd_image_version(chd) >= 5); int padded = (frames + CD_TRACK_PADDING - 1) / CD_TRACK_PADDING; Offset += padded * CD_TRACK_PADDING; @@ -334,7 +365,7 @@ bool CHDDisc::TryOpen(const char* file) } } - start_prefetch(file); + start_streams(file); return true; } diff --git a/core/imgread/hunk_prefetch.h b/core/imgread/hunk_prefetch.h deleted file mode 100644 index 270dcb7a..00000000 --- a/core/imgread/hunk_prefetch.h +++ /dev/null @@ -1,151 +0,0 @@ -#pragma once -/* Read-ahead for hunk-compressed disc images. - * - * A worker thread decodes the hunk the reader is expected to want next, - * so a sequential read finds it already decoded. Decoding is deterministic, - * so the reader gets the same bytes whichever thread produced them; only - * the CPU time moves. - * - * The two threads share three hunk buffers through a cTripleBuffer and - * never wait for one another: the reader either finds the hunk it asked - * for or decodes it itself, and the worker sleeps on an eventcount until - * the reader aims it at a new hunk. No lock is taken anywhere. */ -#include "types.h" -#include "lockfree.h" - -struct HunkPrefetch -{ - /* Decode @hunk into @dst on the worker thread. false: could not. */ - typedef bool (*ReadFn)(void *opaque, u32 hunk, u8 *dst); - - static const u32 NO_HUNK = 0xFFFFFFFFu; - - HunkPrefetch() : thread(NULL) - { - buf[0].mem = buf[1].mem = buf[2].mem = NULL; - } - ~HunkPrefetch() { Stop(); } - - bool Running() const { return thread != NULL; } - - bool Start(ReadFn read_fn, void *read_opaque, u32 hunkbytes, u32 hunk_count) - { - if (thread != NULL) - return false; - for (int i = 0; i < 3; i++) - { - buf[i].mem = new u8[hunkbytes]; - buf[i].hunk = NO_HUNK; - } - tri.Reset(); - read = read_fn; - opaque = read_opaque; - total_hunks = hunk_count; - retro_atomic_int_init(&request, (int)NO_HUNK); - retro_atomic_int_init(&ready, (int)NO_HUNK); - retro_atomic_int_init(&stop, 0); - thread = sthread_create(worker_entry, this); - if (thread == NULL) - free_buffers(); - return thread != NULL; - } - - void Stop() - { - if (thread == NULL) - return; - retro_atomic_store_release_int(&stop, 1); - ec.Notify(); - sthread_join(thread); - thread = NULL; - free_buffers(); - } - - /* Reader: the buffer the reader owns right now. Fetch() may change - * which one that is. */ - u8* Buffer() { return buf[tri.Front()].mem; } - - /* Reader: true when Buffer() now holds @hunk, decoded by the worker. - * false when it does not, and what Buffer() holds is then unknown; - * the reader decodes into it itself. Either way the worker is aimed - * at the hunk after @hunk. */ - bool Fetch(u32 hunk) - { - bool hit = false; - - /* ready only says a take is worth trying; the buffer's own tag, - * read once the buffer is ours, is what counts. */ - if ((u32)retro_atomic_load_acquire_int(&ready) == hunk && tri.Take()) - hit = buf[tri.Front()].hunk == hunk; - - retro_atomic_store_release_int(&request, (int)(hunk + 1)); - ec.Notify(); - return hit; - } - -private: - struct - { - u8 *mem; - u32 hunk; /* written by the worker before it publishes the buffer */ - } buf[3]; - cTripleBuffer tri; - retro_atomic_int_t request; /* hunk the reader wants decoded ahead */ - retro_atomic_int_t ready; /* hunk the worker published last */ - retro_atomic_int_t stop; - cEventCount ec; - sthread_t *thread; - ReadFn read; - void *opaque; - u32 total_hunks; - - void free_buffers() - { - for (int i = 0; i < 3; i++) - { - delete [] buf[i].mem; - buf[i].mem = NULL; - } - } - - static void worker_entry(void *p) { ((HunkPrefetch *)p)->worker(); } - - void worker() - { - /* The last request answered, decoded or not: it is not tried - * again until the reader asks for something else. */ - u32 done = NO_HUNK; - - for (;;) - { - u32 req; - - for (;;) - { - int key; - - if (retro_atomic_load_acquire_int(&stop)) - return; - req = (u32)retro_atomic_load_acquire_int(&request); - if (req != NO_HUNK && req != done) - break; - - key = ec.Prepare(); - req = (u32)retro_atomic_load_acquire_int(&request); - if (retro_atomic_load_acquire_int(&stop) - || (req != NO_HUNK && req != done)) - ec.Cancel(); - else - ec.Commit(key); - } - - if (req < total_hunks && read(opaque, req, buf[tri.Back()].mem)) - { - buf[tri.Back()].hunk = req; - retro_atomic_store_release_int(&ready, (int)req); - tri.Publish(); - } - done = req; - } - } -}; diff --git a/core/libretro-common/include/vfs/vfs_prefetch.h b/core/libretro-common/include/vfs/vfs_prefetch.h new file mode 100644 index 00000000..e48f90e8 --- /dev/null +++ b/core/libretro-common/include/vfs/vfs_prefetch.h @@ -0,0 +1,102 @@ +/* Copyright (C) 2026 The RetroArch team + * + * --------------------------------------------------------------------------------------- + * The following license statement only applies to this file (vfs_prefetch.h). + * --------------------------------------------------------------------------------------- + * + * Permission is hereby granted, free of charge, + * to any person obtaining a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef __LIBRETRO_SDK_VFS_PREFETCH_H +#define __LIBRETRO_SDK_VFS_PREFETCH_H + +#include +#include + +#include + +RETRO_BEGIN_DECLS + +/* A background prefetcher for a file read over a network: a thread of + * its own fetches the windows ahead of the reader on a connection of + * its own, so a run of sequential reads is served from memory and the + * reading thread never waits for the network. The reader's side is + * lock-free: a read copies from a ready window or reports a miss (and + * the caller reads the network itself, as it would without this), and + * a seek out of the fetched range retargets the thread. + * + * Only built with HAVE_THREADS; without it vfs_prefetch_new() returns + * NULL and everything else is a no-op, so callers keep one code path. */ + +struct vfs_prefetch; + +/* Read @len octets at @off into @buf; the octets read (short at end of + * file), -1 on failure. Runs on the prefetch thread, on that thread's + * own connection. */ +typedef int64_t (*vfs_prefetch_fetch_t)(void *user, uint64_t off, uint8_t *buf, size_t len); + +/** + * vfs_prefetch_new: + * @window : octets per window (a client's read-ahead size fits) + * @slots : windows kept ahead, 2 or more; memory is window x slots + * @start : the offset to begin at (0 for a fresh file) + * + * Starts the thread. NULL when threads are unavailable or memory is. + **/ +struct vfs_prefetch *vfs_prefetch_new(vfs_prefetch_fetch_t fetch, void *user, + size_t window, unsigned slots, uint64_t start); + +/** + * vfs_prefetch_read: + * + * Copy up to @len octets at @off from the fetched windows. Returns the + * octets copied (fewer than @len when the range runs past what is + * ready, 0 when a ready window says the file ends there), or -1 when + * nothing at @off is ready: the caller then reads the network itself. + * Consumed windows are recycled as the reader moves past them. + **/ +int64_t vfs_prefetch_read(struct vfs_prefetch *p, uint64_t off, uint8_t *buf, size_t len); + +/** + * vfs_prefetch_view: + * + * vfs_prefetch_read() without the copy: points @ptr at the octets at + * @off in a ready window and returns how many follow there, to the end + * of that window; 0 at the end of the file, -1 on a miss, which + * retargets the thread as a read's miss does. Only the reader recycles + * windows, so @ptr stays valid until its next read, view or seek. + **/ +int64_t vfs_prefetch_view(struct vfs_prefetch *p, uint64_t off, const uint8_t **ptr); + +/** + * vfs_prefetch_seek: + * + * The reader will continue at @off: when that is outside what is + * fetched or being fetched, the thread starts over there. + **/ +void vfs_prefetch_seek(struct vfs_prefetch *p, uint64_t off); + +/** + * vfs_prefetch_free: + * + * Stops the thread (waiting for a fetch in flight to end) and frees + * everything. @user is the caller's to release afterwards. + **/ +void vfs_prefetch_free(struct vfs_prefetch *p); + +RETRO_END_DECLS + +#endif diff --git a/core/libretro-common/vfs/vfs_prefetch.c b/core/libretro-common/vfs/vfs_prefetch.c new file mode 100644 index 00000000..f9f2f6d6 --- /dev/null +++ b/core/libretro-common/vfs/vfs_prefetch.c @@ -0,0 +1,412 @@ +/* Copyright (C) 2026 The RetroArch team + * + * --------------------------------------------------------------------------------------- + * The following license statement only applies to this file (vfs_prefetch.c). + * --------------------------------------------------------------------------------------- + * + * Permission is hereby granted, free of charge, + * to any person obtaining a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include +#include + +#include + +#ifdef HAVE_THREADS + +#include +#include +#include + +/* Slot states. The thread moves EMPTY -> FILLING -> READY; only the + * reader moves READY -> EMPTY (a window it has passed, or one from a + * stream it left), so every write the thread makes to a slot comes + * after the reader's CAS and every read the reader makes comes after + * the thread's release: no field is ever touched from both sides at + * once, and neither side holds a lock. */ +#define SLOT_EMPTY 0 +#define SLOT_FILLING 1 +#define SLOT_READY 2 + +struct vfs_prefetch_slot +{ + uint8_t *buf; + uint64_t off; + size_t len; + int gen; /* the stream this window belongs to */ + int eof; /* the file ends inside this window */ + retro_atomic_int_t state; +}; + +struct vfs_prefetch +{ + vfs_prefetch_fetch_t fetch; + void *user; + size_t window; + unsigned nslots; + struct vfs_prefetch_slot *slot; + + /* The stream: a generation and the window index it starts at + * (windows are aligned to their size, so an index is enough and it + * fits an atomic int: 2^31 windows of 1 MiB). The reader stores the + * target index then bumps gen (release); the thread loads gen + * (acquire) and then the index. Windows carry the gen they were + * fetched under, so the reader ignores stale ones and the thread + * recycles them when it notices the bump. */ + retro_atomic_int_t gen; + retro_atomic_int_t target_idx; + /* the window index (offset / window) the thread fetches next, so the + * reader can tell "not yet fetched" from "elsewhere" without a lock */ + retro_atomic_int_t next_idx; + /* the gen next_idx belongs to: until the thread has taken a + * retarget, the stream is headed for target_idx instead */ + retro_atomic_int_t seen_gen; + retro_atomic_int_t stop; + + retro_eventcount_t wake; /* the thread parks here */ + sthread_t *thread; + int thread_gen; + uint64_t next_off; + int at_eof; +}; + +static void vfs_prefetch_thread(void *data) +{ + struct vfs_prefetch *p = (struct vfs_prefetch*)data; + + for (;;) + { + unsigned i; + int gen; + struct vfs_prefetch_slot *s = NULL; + int64_t n; + + if (retro_atomic_load_acquire_int(&p->stop)) + return; + + /* a retarget: start over there; the reader recycles the windows + * of the old stream itself as it finds them */ + gen = retro_atomic_load_acquire_int(&p->gen); + if (gen != p->thread_gen) + { + p->thread_gen = gen; + p->next_off = (uint64_t)retro_atomic_load_acquire_int(&p->target_idx) * p->window; + p->at_eof = 0; + retro_atomic_store_release_int(&p->next_idx, (int)(p->next_off / p->window)); + retro_atomic_store_release_int(&p->seen_gen, gen); + } + + /* work: an empty slot while the stream has not ended */ + if (!p->at_eof) + { + for (i = 0; i < p->nslots; i++) + { + if (retro_atomic_cas_int(&p->slot[i].state, SLOT_EMPTY, SLOT_FILLING)) + { + s = &p->slot[i]; + break; + } + } + } + + if (!s) + { + /* nothing to do until the reader frees a window or seeks */ + int key = retro_eventcount_prepare_wait(&p->wake); + if (retro_atomic_load_acquire_int(&p->stop) + || retro_atomic_load_acquire_int(&p->gen) != p->thread_gen) + { + retro_eventcount_cancel_wait(&p->wake); + continue; + } + if (!p->at_eof) + { + for (i = 0; i < p->nslots; i++) + if (retro_atomic_load_acquire_int(&p->slot[i].state) == SLOT_EMPTY) + break; + if (i < p->nslots) + { + retro_eventcount_cancel_wait(&p->wake); + continue; + } + } + retro_eventcount_commit_wait(&p->wake, key); + continue; + } + + s->off = p->next_off; + s->gen = p->thread_gen; + n = p->fetch(p->user, s->off, s->buf, p->window); + if (n < 0) + { + /* a failed fetch: give the slot back; the reader's own read + * will report the failure, and this thread tries again later */ + retro_atomic_store_release_int(&s->state, SLOT_EMPTY); + p->at_eof = 1; /* stop hammering; a seek restarts */ + continue; + } + s->len = (size_t)n; + s->eof = (size_t)n < p->window; + if (s->eof) + p->at_eof = 1; + p->next_off += (uint64_t)n; + retro_atomic_store_release_int(&p->next_idx, (int)(p->next_off / p->window)); + retro_atomic_store_release_int(&s->state, SLOT_READY); + } +} + +struct vfs_prefetch *vfs_prefetch_new(vfs_prefetch_fetch_t fetch, void *user, + size_t window, unsigned slots, uint64_t start) +{ + struct vfs_prefetch *p; + unsigned i; + + if (!fetch || !window || slots < 2) + return NULL; + if (!(p = (struct vfs_prefetch*)calloc(1, sizeof(*p)))) + return NULL; + p->fetch = fetch; + p->user = user; + p->window = window; + p->nslots = slots; + if (!(p->slot = (struct vfs_prefetch_slot*)calloc(slots, sizeof(*p->slot)))) + { + free(p); + return NULL; + } + for (i = 0; i < slots; i++) + { + if (!(p->slot[i].buf = (uint8_t*)malloc(window))) + { + while (i--) + free(p->slot[i].buf); + free(p->slot); + free(p); + return NULL; + } + retro_atomic_store_release_int(&p->slot[i].state, SLOT_EMPTY); + } + retro_atomic_store_release_int(&p->target_idx, (int)(start / window)); + /* gen 1 with the target set: the thread sees a retarget and starts there */ + p->thread_gen = 0; + retro_atomic_store_release_int(&p->gen, 1); + retro_atomic_store_release_int(&p->next_idx, (int)(start / window)); + retro_atomic_store_release_int(&p->seen_gen, 0); + retro_atomic_store_release_int(&p->stop, 0); + if (!retro_eventcount_init(&p->wake)) + { + for (i = 0; i < slots; i++) + free(p->slot[i].buf); + free(p->slot); + free(p); + return NULL; + } + if (!(p->thread = sthread_create(vfs_prefetch_thread, p))) + { + retro_eventcount_free(&p->wake); + for (i = 0; i < slots; i++) + free(p->slot[i].buf); + free(p->slot); + free(p); + return NULL; + } + return p; +} + +/* Recycles the windows the reader has moved past, and any left from a + * stream it seeked away from; returns the current generation. */ +static int vfs_prefetch_recycle(struct vfs_prefetch *p, uint64_t off) +{ + int gen = retro_atomic_load_acquire_int(&p->gen); + int freed = 0; + unsigned i; + + for (i = 0; i < p->nslots; i++) + { + struct vfs_prefetch_slot *s = &p->slot[i]; + if (retro_atomic_load_acquire_int(&s->state) == SLOT_READY + && (s->gen != gen || (!s->eof && s->off + s->len <= off))) + { + if (retro_atomic_cas_int(&s->state, SLOT_READY, SLOT_EMPTY)) + freed = 1; + } + } + if (freed) + retro_eventcount_notify(&p->wake); + return gen; +} + +int64_t vfs_prefetch_view(struct vfs_prefetch *p, uint64_t off, const uint8_t **ptr) +{ + int gen; + unsigned i; + + if (!p || !ptr) + return -1; + gen = vfs_prefetch_recycle(p, off); + for (i = 0; i < p->nslots; i++) + { + struct vfs_prefetch_slot *s = &p->slot[i]; + if (retro_atomic_load_acquire_int(&s->state) != SLOT_READY + || s->gen != gen) + continue; + if (off >= s->off && off < s->off + s->len) + { + *ptr = s->buf + (off - s->off); + return (int64_t)(s->off + s->len - off); + } + if (s->eof && off >= s->off + s->len) + return 0; + } + vfs_prefetch_seek(p, off); + return -1; +} + +int64_t vfs_prefetch_read(struct vfs_prefetch *p, uint64_t off, uint8_t *buf, size_t len) +{ + int gen; + unsigned i; + size_t done = 0; + + if (!p) + return -1; + gen = vfs_prefetch_recycle(p, off); + + /* copy from ready windows covering @off, across as many as touch */ + while (done < len) + { + struct vfs_prefetch_slot *hit = NULL; + uint64_t at = off + done; + for (i = 0; i < p->nslots; i++) + { + struct vfs_prefetch_slot *s = &p->slot[i]; + if (retro_atomic_load_acquire_int(&s->state) == SLOT_READY + && s->gen == gen && at >= s->off && at < s->off + s->len) + { + hit = s; + break; + } + /* a ready window that says the file ends before @at */ + if (retro_atomic_load_acquire_int(&s->state) == SLOT_READY + && s->gen == gen && s->eof && at >= s->off + s->len) + return (int64_t)done; + } + if (!hit) + { + /* a miss: if the thread is behind or elsewhere, it starts over + * here rather than fetching what the reader pulls itself */ + if (!done) + vfs_prefetch_seek(p, off); + break; + } + { + size_t take = (size_t)(hit->off + hit->len - at); + if (take > len - done) + take = len - done; + memcpy(buf + done, hit->buf + (at - hit->off), take); + done += take; + if (hit->eof && at + take >= hit->off + hit->len) + return (int64_t)done; + } + } + return done ? (int64_t)done : -1; +} + +void vfs_prefetch_seek(struct vfs_prefetch *p, uint64_t off) +{ + int gen; + unsigned i; + int next; + + if (!p) + return; + gen = retro_atomic_load_acquire_int(&p->gen); + /* covered by a ready window: nothing to do */ + for (i = 0; i < p->nslots; i++) + { + struct vfs_prefetch_slot *s = &p->slot[i]; + if (retro_atomic_load_acquire_int(&s->state) == SLOT_READY + && s->gen == gen && off >= s->off && off < s->off + s->len) + return; + } + /* about to be fetched (the window being filled or the next one), + * judged by where the stream is headed: a retarget the thread has + * not taken yet replaces where it was */ + next = retro_atomic_load_acquire_int(&p->seen_gen) == gen + ? retro_atomic_load_acquire_int(&p->next_idx) + : retro_atomic_load_acquire_int(&p->target_idx); + if (off / p->window + 1 >= (uint64_t)next && off / p->window <= (uint64_t)next + 1) + return; + retro_atomic_store_release_int(&p->target_idx, (int)(off / p->window)); + retro_atomic_store_release_int(&p->gen, gen + 1); + /* the old stream's ready windows go back now, so the thread has + * room to start; ones still filling come back through read() */ + for (i = 0; i < p->nslots; i++) + { + struct vfs_prefetch_slot *s = &p->slot[i]; + if (retro_atomic_load_acquire_int(&s->state) == SLOT_READY) + retro_atomic_cas_int(&s->state, SLOT_READY, SLOT_EMPTY); + } + retro_eventcount_notify(&p->wake); +} + +void vfs_prefetch_free(struct vfs_prefetch *p) +{ + unsigned i; + if (!p) + return; + retro_atomic_store_release_int(&p->stop, 1); + retro_eventcount_notify(&p->wake); + sthread_join(p->thread); + retro_eventcount_free(&p->wake); + for (i = 0; i < p->nslots; i++) + free(p->slot[i].buf); + free(p->slot); + free(p); +} + +#else /* !HAVE_THREADS */ + +struct vfs_prefetch *vfs_prefetch_new(vfs_prefetch_fetch_t fetch, void *user, + size_t window, unsigned slots, uint64_t start) +{ + (void)fetch; (void)user; (void)window; (void)slots; (void)start; + return NULL; +} + +int64_t vfs_prefetch_read(struct vfs_prefetch *p, uint64_t off, uint8_t *buf, size_t len) +{ + (void)p; (void)off; (void)buf; (void)len; + return -1; +} + +int64_t vfs_prefetch_view(struct vfs_prefetch *p, uint64_t off, const uint8_t **ptr) +{ + (void)p; (void)off; (void)ptr; + return -1; +} + +void vfs_prefetch_seek(struct vfs_prefetch *p, uint64_t off) +{ + (void)p; (void)off; +} + +void vfs_prefetch_free(struct vfs_prefetch *p) +{ + (void)p; +} + +#endif diff --git a/jni/Android.mk b/jni/Android.mk index 23b94eb5..48afdab6 100644 --- a/jni/Android.mk +++ b/jni/Android.mk @@ -56,6 +56,8 @@ COREFLAGS += -DNDEBUG -DNO_VERIFY ifeq ($(NO_THREADS),1) COREFLAGS += -DTARGET_NO_THREADS +else +COREFLAGS += -DHAVE_THREADS endif ifeq ($(HAVE_GENERIC_JIT),1) diff --git a/tools/chd/chd_streams_test.cpp b/tools/chd/chd_streams_test.cpp new file mode 100644 index 00000000..56477950 --- /dev/null +++ b/tools/chd/chd_streams_test.cpp @@ -0,0 +1,186 @@ +/* CD audio and data read from a CHD at once, as a game plays music while + * it streams from the disc. + * + * chd_streams_test + * + * The image is the two-track CD tools/chd/make_fixture.py writes (data, + * then audio). The audio is read a sector at a time, steadily, and the + * data in bursts announced with Prefetch() as the GD-ROM's read command + * does, between pauses that stand in for emulation. The pauses are + * scaled to how fast this build decodes, so the reads are paced like the + * drive's, well below the decoder, under a sanitizer as without. Every + * sector must match the BIN it came from, and the hunks must be decoded + * by the read-ahead threads: the reading thread may decode only where a + * stream starts. chd_image_read_hunk is wrapped (run.sh links with + * --wrap) to count the decodes done on the reading thread. + */ +#include +#include +#include + +#include +#include +#include + +#include "types.h" +#include "imgread/common.h" +#include "imgread/chd_image.h" + +Disc* chd_parse(const char* file); + +u8 q_subchannel[96]; + +void GenericLog(LogTypes::LOG_LEVELS, LogTypes::LOG_TYPE, const char*, int, + const char* fmt, ...) +{ + (void)fmt; +} + +static uintptr_t reader_thread; +static long reader_decodes; +static long decodes; + +extern "C" bool __real_chd_image_read_hunk(chd_image_t *img, uint32_t hunk, uint8_t *dst); + +extern "C" bool __wrap_chd_image_read_hunk(chd_image_t *img, uint32_t hunk, uint8_t *dst) +{ + if (sthread_get_current_thread_id() == reader_thread) + reader_decodes++; + __sync_fetch_and_add(&decodes, 1); + return __real_chd_image_read_hunk(img, hunk, dst); +} + +#define BURSTS 12 +#define BURST_SECTORS 96 +#define AUDIO_STEPS 40 /* audio sectors between two bursts */ + +static retro_time_t pace_us; /* emulation between two sector reads */ +static retro_time_t owed_us; + +static void pace(retro_time_t us) +{ + owed_us += us; + if (owed_us >= 1000) + { + retro_sleep((unsigned)(owed_us / 1000)); + owed_us %= 1000; + } +} + +/* Microseconds this build takes to decode a hunk, best of a few. */ +static retro_time_t decode_us(const char *path) +{ + chd_image_t *img = chd_image_open(path, NULL, 0); + retro_time_t best = 0; + u8 *buf; + u32 h; + + if (!img) + return 1000; + buf = (u8 *)malloc(chd_image_hunk_bytes(img)); + for (h = 0; h < 16 && h < chd_image_hunk_count(img); h++) + { + retro_time_t t0 = cpu_features_get_time_usec(), t; + __real_chd_image_read_hunk(img, h, buf); + t = cpu_features_get_time_usec() - t0; + if (!best || t < best) + best = t; + } + free(buf); + chd_image_close(img); + return best ? best : 1; +} + +int main(int argc, char **argv) +{ + FILE *f; + u8 *bin; + long bin_len; + Disc *disc; + u8 sector[2352]; + SectorFormat fmt; + SubcodeFormat sub; + u32 data_fad, audio_fad, data_lba, audio_lba, data_end, audio_end; + int bad = 0, b, i; + long expected; + + if (argc < 3) + { + fprintf(stderr, "usage: %s image.chd image.bin\n", argv[0]); + return 2; + } + if (!(f = fopen(argv[2], "rb"))) + return 2; + fseek(f, 0, SEEK_END); + bin_len = ftell(f); + fseek(f, 0, SEEK_SET); + bin = (u8 *)malloc(bin_len); + if (fread(bin, 1, bin_len, f) != (size_t)bin_len) + return 2; + fclose(f); + + /* a hunk holds 8 sectors: read one every half hunk's decode time, + * at least 50 us, a quarter of the speed the decoder keeps up with */ + pace_us = decode_us(argv[1]) / 2; + if (pace_us < 50) + pace_us = 50; + reader_thread = sthread_get_current_thread_id(); + if (!(disc = chd_parse(argv[1])) || disc->tracks.size() != 2 + || disc->tracks[1].CTRL != 0) + { + printf("FAIL: open as a data track and an audio track\n"); + return 1; + } + /* LBA in the BIN = FAD - 150 */ + data_fad = disc->tracks[0].StartFAD; + data_end = disc->tracks[0].EndFAD; + audio_fad = disc->tracks[1].StartFAD; + audio_end = disc->tracks[1].EndFAD; + + /* let the threads settle after the open */ + retro_sleep(50); + reader_decodes = 0; + + for (b = 0; b < BURSTS; b++) + { + for (i = 0; i < AUDIO_STEPS && audio_fad <= audio_end; i++, audio_fad++) + { + audio_lba = audio_fad - 150; + if (!disc->ReadSector(audio_fad, sector, &fmt, NULL, &sub) + || memcmp(sector, bin + (size_t)audio_lba * 2352, 2352)) + bad++; + pace(pace_us); + } + /* the drive seeks before the first sector comes */ + disc->Prefetch(data_fad, BURST_SECTORS); + pace(pace_us * 16); + for (i = 0; i < BURST_SECTORS && data_fad <= data_end; i++, data_fad++) + { + data_lba = data_fad - 150; + if (!disc->ReadSector(data_fad, sector, &fmt, NULL, &sub) + || memcmp(sector, bin + (size_t)data_lba * 2352, 2352)) + bad++; + pace(pace_us); + } + } + + /* the threads stop with the disc, and their count with them */ + delete disc; + + /* A decode here as each stream starts, and room for a thread now and + * then late; reads that throw out each other's hunks decode here at + * least twice a burst. */ + expected = 2 + BURSTS / 2; + printf(" %ld hunks decoded, %ld of them on the reading thread " + "(at most %ld wanted)\n", decodes, reader_decodes, expected); + if (bad) + printf("FAIL: %d sectors do not match the BIN\n", bad); + if (reader_decodes > expected) + printf("FAIL: audio and data reads decode on the reading thread\n"); + + free(bin); + if (bad || reader_decodes > expected) + return 1; + printf("chd_streams_test: PASS\n"); + return 0; +} diff --git a/tools/chd/run.sh b/tools/chd/run.sh index 5078d68d..acfcd992 100755 --- a/tools/chd/run.sh +++ b/tools/chd/run.sh @@ -13,6 +13,11 @@ # The preprocessor flags are read back out of the core's own build, so the # test decodes with exactly the codec set the core is built with. # +# chd_streams_test then reads CD audio and data from the CD at once, as a +# game playing music while it streams does, through the core's chd.cpp, +# under ASan + UBSan and under TSan: the read-ahead threads must decode the +# hunks, and neither stream throw out the other's. +# # Needs chdman (MAME tools) and python3. Fails rather than skips when either # is missing: a CHD check that quietly does nothing is how a codec stops # working without anyone noticing. @@ -64,6 +69,39 @@ $CC -O1 -g $VDEFS -fsanitize=address,undefined \ -lm -lpthread done +CXX=${CXX:-c++} +STREAM_SRC="core/imgread/chd_image.c core/deps/coreio/coreio.c + core/archive/archive.c $L/formats/chd/rchd.c + $L/encodings/encoding_huffman.c $L/encodings/encoding_rzstd.c + $L/encodings/encoding_deflate.c $L/encodings/encoding_crc32.c + $L/formats/7z/r7z_lzma.c $L/formats/7z/r7z_archive.c + $L/formats/7z/r7z_lzma2.c $L/formats/7z/r7z_lzma_stream.c + $L/formats/7z/r7z_bcj2.c $L/formats/7z/r7z_filters.c + $L/formats/zip/rzip_archive.c $L/formats/rar/rrar_archive.c + $L/formats/rar/rrar_ppmd7.c $L/rthreads/rthreads.c + $L/rthreads/retro_eventcount.c $L/formats/flac/rflac.c + $L/features/features_cpu.c $L/streams/file_stream.c + $L/vfs/vfs_implementation.c $L/vfs/vfs_prefetch.c $L/file/file_path.c + $L/file/file_path_io.c $L/file/retro_dirent.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 + $L/time/rtime.c $L/memmap/memalign.c" +for san in address,undefined thread; do + v=$(echo $san | tr -d ,) + mkdir -p "$WORK/o_$v" + for s in $STREAM_SRC; do + $CC -O1 -g $DEFS -fsanitize=$san -fno-sanitize-recover=undefined \ + -Icore -Icore/libretro -Icore/deps -I$L/include \ + -c "$s" -o "$WORK/o_$v/$(echo "$s" | tr / _).o" + done + $CXX -std=c++11 -fpermissive -w -O1 -g $DEFS -fsanitize=$san \ + -fno-sanitize-recover=undefined \ + -Icore -Icore/libretro -Icore/deps -I$L/include \ + -o "$WORK/chd_streams_test_$v" tools/chd/chd_streams_test.cpp \ + core/imgread/chd.cpp "$WORK/o_$v"/*.o \ + -Wl,--wrap=chd_image_read_hunk -lm -lpthread +done + python3 tools/chd/make_fixture.py "$WORK/base" python3 tools/chd/make_fixture.py "$WORK/child" --variant 1 python3 tools/chd/make_fixture.py "$WORK/gd/disc" --gdi @@ -117,8 +155,17 @@ for c in cdzs cdlz; do done check "$WORK/chain/child.chd" "$WORK/child.bin" +export TSAN_OPTIONS=halt_on_error=1 +for c in cdzs cdfl; do + for v in addressundefined thread; do + "$WORK/chd_streams_test_$v" "$WORK/$c.chd" "$WORK/base.bin" | tail -1 \ + | grep -q PASS || { echo "FAIL (streams, $v): $c"; fail=1; } + done +done + if [ $fail = 0 ]; then echo "chd_image_test: all images PASS (mapped and bounce-buffer builds)" + echo "chd_streams_test: CD audio and data read at once PASS" else exit 1 fi diff --git a/tools/static-link-check.sh b/tools/static-link-check.sh index c5fbf37f..777a1aa4 100644 --- a/tools/static-link-check.sh +++ b/tools/static-link-check.sh @@ -34,7 +34,7 @@ sort -u -o "$WORK/have" "$WORK/have" # Only the symbols the core's own headers name: libc, GL and the C++ # runtime come from the toolchain. nm -u "$A" 2>/dev/null | awk '{print $2}' | sort -u \ - | grep -E '^(core_|archive_|chd_image_|rchd_|r7z_|rzip_|rinflate_|rzstd_|rflac_|filestream_|retro_vfs_|path_|fill_pathname|string_|strlcpy|strlcat|encoding_|cpu_features_|retro_opendir|retro_readdir|retro_closedir|retro_dirent|sthread_|slock_|scond_|retro_eventcount|retro_spsc|memalign_|rtime_|utf8|utf16)' \ + | grep -E '^(core_|archive_|chd_image_|rchd_|r7z_|rzip_|rinflate_|rzstd_|rflac_|filestream_|retro_vfs_|path_|fill_pathname|string_|strlcpy|strlcat|encoding_|cpu_features_|retro_opendir|retro_readdir|retro_closedir|retro_dirent|sthread_|slock_|scond_|retro_eventcount|retro_spsc|vfs_prefetch_|memalign_|rtime_|utf8|utf16)' \ > "$WORK/need" || true MISSING=$(comm -23 "$WORK/need" "$WORK/have") if [ -n "$MISSING" ]; then diff --git a/tools/threads/threads_test.cpp b/tools/threads/threads_test.cpp index 756e8c42..a23048cb 100644 --- a/tools/threads/threads_test.cpp +++ b/tools/threads/threads_test.cpp @@ -19,7 +19,6 @@ #include "lockfree.h" #include "libretro/emu_baton.h" #include "libretro/input_latch.h" -#include "imgread/hunk_prefetch.h" static int failures; @@ -308,75 +307,6 @@ static void test_mpsc_list(void) CHECK(handled == (long)MPSC_PRODUCERS * MPSC_REQUESTS); } -/* ---- HunkPrefetch ---------------------------------------------------- */ - -#define HUNK_BYTES 4096 -#define HUNK_COUNT 512 -#define HUNK_READS 60000 - -static bool hunk_is_bad(u32 hunk) { return hunk % 97 == 96; } - -static bool hunk_decode(void *, u32 hunk, u8 *dst) -{ - if (hunk_is_bad(hunk)) - return false; - for (u32 i = 0; i < HUNK_BYTES; i++) - dst[i] = (u8)(hunk * 31 + i); - return true; -} - -static bool hunk_matches(u32 hunk, const u8 *p) -{ - for (u32 i = 0; i < HUNK_BYTES; i++) - if (p[i] != (u8)(hunk * 31 + i)) - return false; - return true; -} - -static void test_hunk_prefetch(void) -{ - HunkPrefetch pf; - unsigned seed = 12345; - u32 hunk = 0; - long hits = 0; - - CHECK(pf.Start(hunk_decode, NULL, HUNK_BYTES, HUNK_COUNT)); - CHECK(!pf.Start(hunk_decode, NULL, HUNK_BYTES, HUNK_COUNT)); - for (int i = 0; i < HUNK_READS; i++) - { - unsigned r = rng_next(&seed); - - /* Mostly sequential, as a disc is read, with a seek now and then. */ - if (r % 50 == 0) - hunk = rng_next(&seed) % HUNK_COUNT; - else - hunk = (hunk + 1) % HUNK_COUNT; - - if (pf.Fetch(hunk)) - { - /* The worker never hands over a hunk it could not decode. */ - CHECK(!hunk_is_bad(hunk)); - CHECK(hunk_matches(hunk, pf.Buffer())); - hits++; - } - else if (hunk_decode(NULL, hunk, pf.Buffer())) - CHECK(hunk_matches(hunk, pf.Buffer())); - - /* Give the worker a moment now and then, as emulation between - * two disc reads does. */ - if (i % 64 == 0) - retro_sleep(1); - } - pf.Stop(); - CHECK(!pf.Running()); - CHECK(hits > 0); - - /* Starts again after a stop. */ - CHECK(pf.Start(hunk_decode, NULL, HUNK_BYTES, HUNK_COUNT)); - pf.Stop(); - printf(" hunk prefetch: %ld of %d reads found decoded\n", hits, HUNK_READS); -} - /* ---- EmuBaton -------------------------------------------------------- */ #define BATON_FRAMES 30000 @@ -643,7 +573,6 @@ int main(void) { "cSlotCache", test_slot_cache }, { "cTripleBuffer", test_triple_buffer }, { "cMpscList", test_mpsc_list }, - { "HunkPrefetch", test_hunk_prefetch }, { "EmuBaton", test_emu_baton }, { "InputLatch", test_input_latch }, { "cMarkSet", test_mark_set },