Commit Graph
86 Commits
Author SHA1 Message Date
LibretroAdmin 4a3f2e1316 custom textures: the renderer works out a texture's names, the loader only reads the request
The loader thread worked out the hash a texture's replacement is filed
under itself: from video memory, which with threaded rendering the game
is writing again by then, and from the texture's own fields, which the
renderer may be updating for the next request. It also looked at the
texture's "dirty" stamp, a plain variable of the renderer's.

The renderer does the hashing now, when it asks - it is updating the
texture at that moment, and the game is held while it does - and leaves
the three names for the loader in the texture before it counts the
request. The loader reads the count and then the names, loads, and
takes its count off: if a request came in meanwhile, the count it took
off is not all there was, it goes round again, and what it loaded from
names that may have been half one request's and half the next's is
dropped. The image and its size are still only looked at by the renderer
once the count is back to zero. Nothing else of a texture is read by the
loader.

(The "dirty" check saved loading for a texture that had been invalidated
since it asked. Such a texture asks again when it is next updated, and
the image loaded for nothing is freed then, or with the texture.)

tools/threads: the model of the loader's queue carries a three-word
request the same way and checks that what was made of each node's last
request is that request's, whole; under TSan and ASan + UBSan.

Run: the test disc with a pack of sixteen magenta replacements for the
textures it dumps, threaded rendering on - the screenshot has 41,148
magenta pixels against 24,228 without the pack; with the option on and
no pack, 24,228.

Not done: the audit's bound on what is queued. A texture is on the list
at most once and holds at most one loaded image, so both are bounded by
the number of textures, not by bytes.
2026-10-08 08:46:00 +02:00
LibretroAdmin 081d1ddd17 rend: a texture that runs past the end of video memory is read to the end of it every time
Shenmue's Space Harrier mini-arcade loads a planar texture that
starts inside video memory and would end outside it, and uses only
the top. The texture cache reads as many lines as there are.

It did that by cutting the texture's height and size down for the
update and putting the height back afterwards. The size stayed cut -
it is what is locked and hashed, and has to end inside video memory -
so the second time the texture was updated nothing ran past the end
any more, nothing was cut, and every line was read, the ones beyond
video memory included. The first update, for its part, converted into
a buffer made for the cut height and then uploaded the full one.

The number of lines to read is now worked out each time from what the
whole texture would take, the height is left alone, the buffer is the
texture's size, and the lines that are not read are cleared. From
upstream ("fix Space Harrier arcade in Shenmue"), which restores the
size after each update instead; here the size has other readers.

The headless and live tests pass and the converter test's 340 results
are unchanged. No texture on the test disc runs past the end.
2026-10-08 05:17:09 +02:00
LibretroAdmin 783264f91c libretro: "Enable RTT (Render To Texture) Buffer" is gone, and the table of games that forced it
The option chose between two ways of treating what a game renders to
a texture: copy it to video memory after every such render, waiting
for the graphics card each time and losing Render To Texture
Upscaling, or leave it on the card and have the games that look for
it in video memory come out wrong. A table switched the first on for
the games known to need it.

Since the last three patches there is one way, and it is right for
both: the picture stays on the card and video memory gets it when it
is first touched there. Nothing is left for the option to choose.

- The core option, in every language, and settings.rend
  .RenderToTextureBuffer.
- The copy-after-every-render paths of all four renderers, with what
  only they used: the Vulkan drawers' second render target and its
  render pass variant, the check for the setting having changed, and
  WriteTextureToVRam() (rend/rtt_watch.cpp packs pixels now).
- The table's column, in the console's table and the arcade one, and
  the 51 rows that had nothing else in them.

Render To Texture Upscaling stays, and now applies to every game.
A frontend that still has the old option saved ignores it.

The live test passes on gl, glcore and vulkan, the headless test
passes.
2026-10-07 19:36:06 +02:00
LibretroAdmin 9d64036cdc rend: a second volume's paletted texture is made colours on the CPU
A paletted texture can be left as palette indices and looked up in the
palette by the shader. The shaders have one place in the palette to
look from, the polygon's first texture's. A polygon with two textures
("two volumes", the second drawn inside a modifier volume) whose
second is paletted had that one looked up from the first one's place -
or, when the first has no palette, drawn as its indices.

The second texture of such a polygon is now always converted to
colours, like any texture, and the per-pixel shaders, the ones that
do two volumes, sample it as that. From upstream ("convert palette
textures for area 1 on cpu").

Nothing changes yet: because of the mistake the next commit fixes, no
texture is left as indices at all. With that fixed and this not, the
live test's two-texture polygon is black in its volume where it has
to be magenta, in the per-pixel OpenGL renderer; with both it passes
on gl, glcore and vulkan.
2026-10-07 16:32:26 +02:00
LibretroAdmin a092eec4e5 rend: 240 lines without interlace, the alpha threshold itself counts, no allocation to show the framebuffer
Two small ones from upstream:

- a framebuffer shown without interlace on a television is 240 lines
  at most, whatever its size register says. Upstream: the Echelon
  intro of Confidential Mission (PAL).

- writing a render back to video memory as 1555, the alpha bit is set
  from the threshold up, not above it. Upstream: a black tile under
  the player's feet in Izumo.

And a framebuffer a game draws itself is converted for the GPU in the
memory textures are converted in, in place of 1.2 MB allocated and
freed every frame.
2026-10-07 15:16:48 +02:00
LibretroAdmin de7d1101e5 rend: a texture is converted in memory that is kept, and a wide stride has room
Every texture update allocated a buffer the size of its texture,
converted into it, handed it to the GPU and freed it; a compressed
mipmapped one allocated a second, of four pixels, for its smallest
level. The conversion now goes into one piece of memory that is kept
from one texture to the next and only grown (at most a 1024x1024
texture with its mipmaps, 5.6 MB), freed with the renderer; growing
it copies nothing, since what is in it is the last texture's. The
four pixels are a local. An update allocates nothing.

On this machine the time is the same - the C library was handing the
same block back each time. What is gone is the allocation itself,
which is not free everywhere.

Two things about planar textures and their stride, found on the way:

- with a stride wider than the texture, the last row was converted
  past the end of the buffer by the difference. There is room for it
  now. Upstream has the same overrun.

- with a narrower one, the rest of each row was never written and
  went to the GPU as whatever the memory held. It is cleared.
2026-10-07 14:56:38 +02:00
LibretroAdmin 7b373116ad rend: a compressed texture's name is hashed without an allocation
The hash of a compressed texture is taken in two pieces, the codebook
and the texture, which with mipmaps are not next to each other. The
state that carries a hash from one piece to the next was allocated and
freed for every texture hashed; it is a local now.
2026-10-07 14:23:07 +02:00
LibretroAdmin 1dfb89b24b rend: five fixes to the texture cache, from upstream
- A compressed texture's size was half its pixels' share and left the
  codebook out. The size is what video memory is watched by: a write
  to the rest of the texture went unseen and the texture stayed as it
  was. It is the codebook and a byte for every four pixels.

  The size was also what the texture's name for custom textures was
  hashed from, so two compressed textures could share a name. The
  name is now the hash of the codebook and of the whole texture, the
  same as upstream's; the old name is kept as a second one to look a
  custom texture up by, so texture packs made before still load.

- A texture both compressed and planar was taken for a mistake and
  its compression ignored. It is decoded as what it is: an entry of
  the codebook is four pixels in a row. Upstream: the Genesis emulator
  in the Teenage Mutant Ninja Turtles collection.

- Compressed and mipmapped, a texture is twiddled whatever the scan
  order bit says. Upstream: corrupted textures and a crash in Star
  Wars Demolition.

- The 1x1 level of a compressed YUV texture is taken from the 2x2
  level; its own bytes are not a colour. Upstream: magenta background
  textures in World Series Baseball 2K2.

- A stride of zero in TEXT_CONTROL means the texture's own width, not
  a texture of no size.

The converters themselves were compared with upstream's first, each
one on the same bytes, and are the same; the new planar compressed
ones were checked the same way (tools/rend, next commit). The live
test passes on gl and glcore; the Soul Calibur state, which is mostly
compressed textures, runs with no texture refused.

One difference from upstream is kept: for a paletted texture the name
still includes the bit upstream leaves out, so the names this core
has given such textures do not change.
2026-10-07 13:47:23 +02:00
LibretroAdmin 409cbff6e9 remove functions nothing calls
The linker was asked which functions of the core no code path reaches
(-ffunction-sections with --gc-sections --print-gc-sections), and each
one it named was then looked up by name across the whole source,
every platform's code and the tools included. These have no caller
anywhere:

  MemWrite32                         hw/pvr/pvr_mem.cpp
  pvr_ReadReg                        hw/pvr/pvr_regs.cpp
  rend_framePending                  hw/pvr/ta_ctx.cpp
  rend_init, rend_resize             hw/pvr/Renderer_if.cpp
  emit_WriteCodeCache                hw/sh4/dyna/driver.cpp
  bm_WriteBlockMap                   hw/sh4/dyna/blockmanager.cpp
  dec_Un_rNrN, dec_Un_frNfrM, dec_rz hw/sh4/sh4_opcode_list.cpp
  cpu_iWarn                          hw/sh4/interpr/sh4_opcodes.cpp
  get_nvmem_data                     hw/holly/sb_mem.cpp
  aica_Term                          hw/aica/aica_if.cpp
  reios_term                         reios/reios.cpp
  cryptoReset                        hw/naomi/decrypt.cpp
  printtoc                           imgread/common.cpp
  dc_start                           nullDC.cpp
  shutdown_thread_pool               rend/TexCache.cpp
  gl_DebugOutput                     rend/gl4/gles.cpp
  libPvr_GetRenderTarget,
  libPvr_GetRenderSurface            libretro/libretro.cpp
  VArray2::LockRegion                libretro/common.cpp
  LogManager::SetEnable              log/LogManagerLibretro.cpp
  set_user_data_dir,
  add_system_config_dir,
  add_system_data_dir                stdclass.cpp, with the three
                                     variables only they wrote

The two *_Term functions were empty. cryptoReset was not: but the
cartridge decryption it would have cleared starts itself afresh
whenever a key or an address is set.

Left alone, though the linker named them on x86-64: what the ARM,
ARM64 and x86 recompilers and the cached interpreter call (rdv_*,
emit_Write32, emit_SetBaseAddr, bm_GetStaleBlock, the block reference
counting, _vmem_ReadMem8SX32/16SX32, shil_opcode_name), cpu_iNimp,
which a build without an MMU calls, mmu_QACR_SQ, which is called but
inlined so that only its separate copy goes unused, and
archive_num_entries, which tools/archive uses.

libARM_Term, also empty and never called, stays for now: its file has
CRLF line endings, and a patch that touches it does not go through a
plain git am.
2026-10-06 14:52:51 +02:00
LibretroAdmin 85fc7c39f3 settings that only ever had one value are that value
A number of settings are given a value once, in LoadSettings(), and
never another: there is no core option behind them and nothing else
writes them. Each is replaced by its value where it was read, and the
branch that value never took is removed.

- dynarec.idleskip, always on: the idle loop detection in the block
  decoder is unconditional, and the branch for it being off is gone.
- dynarec.unstable_opt, always off: the store queue shortcut in the
  ARM recompiler that it guarded is removed.
- pvr.MaxThreads, always 3: a constant in the texture upscaler.
- pvr.subdivide_transp, always off, and only read inside an #if 0
  block of the triangle sorter, which goes with it.
- rend.Clipping, always on: tile clipping no longer asks.
- rend.AutoExtraDepthScale, always on.
- debug.SerialConsole, always off: the serial port's echo to stdout.
- imgread.PatchRegion, never set: the two functions that would have
  rewritten a disc's region strings returned at once, and are removed
  with the calls to them.
- validate.OpenGlChecks and pvr.Emulation.zMin/zMax were written and
  never read.

LoadSettings() also loses a block for builds that are not libretro,
which this core is not, and which set a field that no longer exists.

core/rend/soft/softrend.cpp, a software renderer that used one of
these settings, was not in any makefile. It is deleted, and its
declaration with it.
2026-10-06 12:08:50 +02:00
LibretroAdmin fc9fcf5f5e texture cache: a paletted texture keeps its palette selection
A texture's control word has two bits, scan order and stride select,
that mean nothing for a twiddled texture, and the cache cleared them
when it took a texture in. For a paletted texture those two bits are
something else: the top two bits of the palette selection, which says
which part of the palette the texture uses.

The decoding went by a copy taken before, so the texture came out
right the first time. But whether a decoded texture has to be made
again because its colours changed is decided from the control word,
and that now named a different part of the palette. A texture using
the upper half of the palette was not decoded again when its own
colours changed, and was when the colours of the lower half did, with
whatever its own part held at that moment.

Soul Calibur's character select shows it. The second player's portrait
uses the upper half: it kept the colours of whatever was in the
palette when it was first decoded and changed to other wrong colours
whenever the first player's portrait, in the lower half, was replaced.
Loading a save state put it right, because that empties the cache.

The bits are now left alone for paletted textures, as upstream flycast
does, and IsMipmapped() no longer takes one of them for the scan order
of a paletted texture.

This has been there since palette textures were first handled on the
GPU in 2020; it is not from the recent renderer work.
2026-10-06 08:15:00 +02:00
LibretroAdmin cf0790a148 palette: convert it on the thread that draws, when it has changed
The palette's conversion to host formats, and the hashes paletted
textures are keyed by, were redone when pal_needs_update said so: a
flag set by the register writes and cleared by palette_update(), which
the emulation thread called when the game started a render.

With Threaded Rendering that is the wrong thread at the wrong time. The
frame before can still be drawing, on the frontend's thread, when the
game starts the next render, and palette_update() rewrote the very
tables that frame's texture decoding was reading. It also converted
from the live registers, which by then may hold the palette of a frame
the renderer is not on yet.

palette_update() is now called by the renderer, for each frame, before
the frame's textures are processed, and reads the copy of the registers
the frame is drawn with; TexCache.cpp reads that copy throughout, as
the renderers do. Whether the tables are stale is decided by comparing
the 4 KB of palette and the control register with what was converted
last, so there is no flag for two threads to share, and a palette
written back unchanged costs no conversion and no upload.
pal_needs_update is gone.

The live test's disc now draws its background with an 8-bit paletted
texture and, after the texture's last repaint, changes the palette
entry it uses to yellow: the yellow screen takes the texture write and
the palette write both. With the conversion taken out the screen is
black and the test fails.
2026-10-06 04:15:03 +02:00
LibretroAdmin 9b5d70d96b retro_atomic for the last std::atomic uses
Three were left in the core's own sources: the count of custom-texture
load requests in BaseTextureCacheData, the reset request flag, and an
unused <atomic> include in gles.h. They are retro_atomic like the rest;
the mirror of the texture counter in tools/threads/threads_test.cpp goes
the same way. No change in behaviour.
2026-10-06 02:34:21 +02:00
LibretroAdmin 39d518d784 texcache: track writes to protected video memory without a lock
The texture cache write-protects the pages of video memory its textures
were read from, and a write to one of them faults. Everything that
followed sat under vramlist_lock: the fault handler on the emulation
thread took it to walk the page's lock list, mark the textures changed,
free the lock blocks and unprotect the page, and the render thread took
it for every texture it locked or deleted. That was a mutex taken
inside a signal handler and held by the other thread across mprotect
calls.

The lock lists now belong to the render thread alone. On a fault the
emulation thread unprotects the page and marks it in a cMarkSet
(core/lockfree.h): the mprotect calls and two atomic operations, with
no lock, no list and no malloc or free. The render thread collects the
marks before it looks at a texture and takes the locks off those pages
itself, which is what marks their textures changed.

Unprotecting before marking is what keeps this sound. A page the
emulation thread has opened up is always marked afterwards, so the
render thread can go on assuming that a page with locks on it is
protected: where it is not, a mark is coming that removes those locks.

The same mutex guarded vmem32's list of protected ranges, which the
emulation thread walked when it mapped video memory into the 32-bit
address space of an MMU game. That list is gone. _vmem_protect_vram()
and _vmem_unprotect_vram() keep one bit per page, and vmem32 protects
the pages of a new mapping whose bit is set.

This was the last user of cMutex, which goes too.

tools/threads/run.sh has one thread mark and another collect under
ThreadSanitizer and checks that no mark is lost. With the mark taken
out of the fault path, tools/threads/live.sh fails on its screenshot:
the screen stays on the colour the texture had before.
2026-10-05 12:54:58 +02:00
libretroadmin 4440aac7af texcache: stop write-protecting frequently-updated textures, hash instead
Measured on VF3tb with the headless harness: sustained SIGSEGVs at 1-2 per frame
(300-560 per 300-frame window), all attributed to VRAM texture write-protection
by per-cause fault counters (ram=0 bm=0 vram=all after boot). The cycle: a game
rewrites a texture every frame; each write hits the mprotect'd page -> signal ->
texture invalidated + page unprotected -> texture re-uploaded on next use ->
page re-protected -> next frame faults again, forever. Each iteration pays the
signal, the invalidation, an unconditional re-conversion/re-upload, and an
mprotect -- host-time cost invisible to emulated cycles, showing up as steady
frame-time jitter. On Windows the per-fault cost is higher still (vectored
exception dispatch).

After FREQUENT_UPDATE_COUNT (8) updates, treat a texture as dynamic: leave its
vram writable and detect changes at lookup time by content hash (XXH32 over the
same range the lock covered, clamped to VRAM like the lock was). If the game
rewrote identical content, the re-upload is skipped entirely -- cheaper in every
case than the fault path it replaces. Static textures never reach the threshold
and keep the zero-cost-until-written protection. RTT locking (ReadRTTBuffer) is
unchanged.

Measured after: fault windows drop from a constant 300-560 to 0 in steady state,
with remaining bursts only where scene changes create new textures that pay
their first 8 updates before converting. dirty stays 0 for hash-mode textures,
so cache eviction of stale entries is unaffected (they are evicted via the
normal paths when deleted or the cache is cleared).
2026-07-04 01:34:19 +02:00
Autechre 37f0dc56f0 Attempt rebase (#1064)
* Backport https://github.com/flyinghead/flycast/commit/b3de6a166df68e972d3bed44d25806123b773673

* Attempted backport of
https://github.com/flyinghead/flycast/commit/632731bedee26e7e786bcfbe83fd09bd6c481958#diff-4db49ea438ebaea8c723c95b43dabedb396272359f838cf31565a45e78815087
- blank screen

* Buildfix
2021-04-12 17:39:32 +02:00
twinaphex dd4aec7e3f Backport https://github.com/flyinghead/flycast/commit/0e20d5976bcb75b2cb69307e3e872b16c645be50 2021-03-05 19:42:11 +01:00
twinaphex d93029dd6e Backport https://github.com/flyinghead/flycast/commit/0077bbe295ed96f7f02cba4e90f8154dc9de84a3#diff-c54991a443c98cd216e1d1de31881c03094df994b7127cfd20aff09bd28e103d 2021-03-05 00:00:41 +01:00
twinaphex b210cdeff9 Backport https://github.com/flyinghead/flycast/commit/3f1326a3ac12f816a89b9aa7e2aacdc1809d55d6 2021-03-04 23:51:44 +01:00
twinaphex 4cea0ec4a5 Backport https://github.com/flyinghead/flycast/commit/7a889803fd86372e729952ec4db4dd4675490629 2021-03-03 17:24:25 +01:00
twinaphex 2db065b7ae Backport https://github.com/flyinghead/flycast/commit/ac7bd2a6a076bd083b65b57d92b8543c8d1f4c08 2021-03-03 17:18:22 +01:00
twinaphex 7f2d63a59b Backport https://github.com/flyinghead/flycast/commit/8905410aa4e91c5fa9e9598c363ad7b97b4be9ff#diff-3b7da0c1b4807b36c5b48747b9ba5e6f70bfe20044247a28463498e1f56145f8 2021-03-03 02:44:34 +01:00
twinaphex 3cb3623562 Backport https://github.com/flyinghead/flycast/commit/93dc87f17f7d41a2ebb827fa26d41b0ad9939566 2021-02-28 23:08:43 +01:00
barbudreadmon f1d2f4ae21 Revert "Add NDEBUG ifndef"
This reverts commit fc87fa9f51.
2020-11-18 18:12:45 +01:00
twinaphex fc87fa9f51 Add NDEBUG ifndef 2020-11-16 02:55:59 +01:00
barbudreadmon 8c76cf5767 use std::lock_guard with cMutex
backport https://github.com/flyinghead/flycast/commit/9826afa063ac4d8ec030c54c643337d6e7225790

further reduce diff with upstream
2020-10-19 08:58:24 +02:00
twinaphex 8fe4298130 Backport https://github.com/flyinghead/flycast/commit/fee28194fbc3f2ba426cad1d78f8aae588d8d63b 2020-10-10 22:55:57 +02:00
Jean-André Santoni 35937768bd Fix OSX build 2020-09-13 14:48:17 +07:00
Flyinghead c2f735809c handle some palette textures on the GPU. vulkan: some linear tiling
get rid of PixerPacker templates
gl4: no need for GLSL precision qualifier in GL4
vk: Refactor base and oit renderers to avoid code dup
vk: Linear tiling is faster for small, frequently updated textures
Fix Hello Kitty crash with empty first TR polygon
Fix Test Drive Le Mans crash on invalid textures
2020-07-11 19:19:58 +02:00
Flyinghead 0e10e86ea9 protect P4 vram to detect texture updates
Issue  #928
fix race condition when deleting a texture
handle 1x1 YUV texture special case
2020-07-06 15:14:54 +02:00
Flyinghead 7a37293bb3 rend: use original palette data to compute palette hash 2020-04-04 10:43:03 +02:00
Flyinghead 6544c9f32d support multi-session cue/bin. mipmap D-adjust only to increase LoD
limit maple schedule time
allow vram 8-bit reads
gl: use common ReadFramebuffer() func
other minor changes
2020-03-28 14:25:52 +01:00
Flyinghead 12d0d10d58 renderer: decrease MipmapD bias
fixes street lights in SA1
2020-03-18 17:44:58 +01:00
Flyinghead 42984c29b1 custom texture: stop loader thread before loading state 2020-03-13 17:53:03 +01:00
Flyinghead fb83d42d2f renderer: generate mipmaps for custom textures 2020-03-11 17:03:31 +01:00
Flyinghead b018b13cfd recreate the gl/vk texture when loading a custom one 2020-02-24 22:50:35 +01:00
Flyinghead 5069ea3482 OSX build fix 2020-02-22 12:04:23 +01:00
Flyinghead e801d24ecd android/arm32 build fix 2020-02-22 08:27:52 +01:00
Flyinghead adc4405183 Handle mipmap D-adjust param. Change custom tex hash and fix dump issue
Handle mipmap D-adjust param (convert to lod bias)
Don't use tex addr in texture hash to avoid duplicates
Disable mipmapping when dumping textures
2020-02-21 16:29:35 +01:00
Flyinghead 84dde700cb fix crash for 1x1 TW textures 2020-02-07 20:42:46 +01:00
Flyinghead bc8a96ae71 restore previous twiddle table for non-square tex 2020-02-07 19:18:51 +01:00
Flyinghead 60a4cf60d1 upload all texture mipmap levels to gpu
implemented for gl/gl4/vulkan/oit
don't auto-generate mipmaps in vulkan
simpler/smaller detwiddle table
2020-02-07 17:39:05 +01:00
Flyinghead 6848b407c5 android build error when !HAVE_TEXUPSCALE 2019-12-11 09:17:06 +01:00
Flyinghead d22e945a29 renderer: add 32bpp conversion routines for bump map textures 2019-12-10 16:16:17 +01:00
Flyinghead 674e0df8db texture corruptions with some wince games
vram wasn't protected in some cases
Worms World Party, Tomb Raider - Final Revelation, Hundred Swords
2019-12-09 13:24:37 +01:00
Flyinghead 6271cc41a6 gles: refactor stuff out of gles renderer
texture cache management, custom textures, triangle sorting
2019-12-03 11:17:47 +01:00
Flyinghead 8cc71803d3 gl: use GL_UNSIGNED_BYTE instead of GL_UNSIGNED_INT_8_8_8_8 2019-09-25 12:14:11 +02:00
Flyinghead 71e291214a renderer: use new logging 2019-08-30 12:33:56 +02:00
Flyinghead acb96779d6 Don't unprotect vram when a texture is updated
Adjacent textures in other pages still need vram protection
wince: disable vmem32 for Marionette Company 1 & 2 and Rune Jade
Disable verify in TA parsing: fix F1 World Grand Prix for Dreamcast
need RTT to vram option:
L.O.L
Miss Moonlight
Eternal Arcadia
Yu Suzuki Game Works vol.1
2019-07-03 00:19:40 +02:00
Flyinghead 5190f8feb4 renderer: support non-integer v-scaling factor
Fixes missing half-screen in Wacky Races (PAL), Buzz Lightyear of Star Command (PAL),
Tom Clancy's Rainbow Six - Rogue Spear + Mission Pack - Urban Operations (PAL), Sega Worldwide Soccer 2000 (PAL)

fix verify failed when terminating or switching renderer
texture corruption resulting from misaligned page unprotection
2019-07-02 23:42:18 +02:00