Commit Graph
1347 Commits
Author SHA1 Message Date
WizzardSK b05c7a51a7 Drop the wx GUI application and the standalone's GitHub workflows
This repository builds the libretro core; the standalone wx application was
built from it only by upstream's workflows. Gone: src/gui/wxgui, the CemuBin
target with its macOS bundle and Windows resources, the wx/GTK and SDL vcpkg
features, dist/linux and dist/windows, the GUI's translations in
bin/resources (the Wii U shared fonts there stay - the core loads them),
src/tools, CMakeSettings.json, and the Build Cemu, Build check, Deploy
release, Calculate Next Version and Generate translation template workflows.
ENABLE_LIBRETRO is now always on; it stays a variable because the rest of the
build, shared with upstream, still asks for it.

At the next upstream sync, a modify/delete conflict on any of these is
resolved by keeping the file deleted.

Built on all seven targets plus iOS and tvOS.
2026-09-26 10:45:33 +02:00
WizzardSK 627802831c Pace the core's audio by what the frontend asks for, and show the game's frame rate
The audio-stall branch, tested at both ends - sco8487 on a Mali phone
running Deus Ex at 12 fps, NNshi on a desktop in Xenoblade, MK8 and BotW
with FPS++ - and squashed here once both were satisfied.

- AX frames are at least 1.7 ms apart in the core too, as on the wall-clock
  path, so a large grant no longer runs thirty-odd audio frames back to back
  and starves the title's streaming voices (Deus Ex repeating a line).
- A retro_run never grants less than one frame's audio (800 samples), which
  a device that spends most of each frame waiting on audio sync needs, and
  never more than has played, so unevenly spaced or faster-than-60 retro_runs
  do not make RetroArch block on audio it has too much of.
- The ring holds a whole grant: at 128 ms a slow device's 110-140 ms grants
  lost 4000-14000 samples a second in heavy scenes; at 341 ms it lost none in
  24 minutes of sco8487's testing.
- Video > Show Game FPS puts the frames the title actually finished on the
  OSD. RetroArch's own counter counts retro_run calls, which stay at 60 while
  a game drops frames.
- log.txt names the build it came from (also cherry-picked earlier).
2026-09-26 10:36:56 +02:00
WizzardSK 7bf6ce49e3 Leave the standalone's input and audio backends out of the libretro core
The core gets its input from the frontend through LibretroControllerProvider,
but InputManager still started the DSU client - a UDP socket and two threads -
and the real-Wiimote provider, three threads scanning HID and Bluetooth, in
every core, and SDL3 was linked in whole for a provider the core never starts.

The libretro build now turns SDL, hidapi, BlueZ, XInput and DirectInput off
and compiles neither the DSU client nor the keyboard provider; SDL3 moves to a
vcpkg feature the core's build does not ask for, like the wx GUI. DirectSound
and XAudio2 are left out on MSVC too, where they were compiled in and never
started. libusb stays: nsyshid passes real USB toy portals through to the
game with it, which the frontend cannot do.

Foundation and AppKit are now linked for the Metal renderer on macOS
explicitly; they used to arrive through SDL3's link interface.

The Linux arm64 core goes from 27.7 MB to 24.6 MB. Built on all seven
targets plus iOS and tvOS, and run with New Super Mario Bros. U in RetroArch.
2026-09-26 10:21:07 +02:00
WizzardSK c31d267c06 Add rules for coding agents [skip ci]
The rules sco8487 and hunterk proposed in the testers' Discord thread:
the main and libretro branches are never deleted or rewritten, features
go on their own branch and are squashed into libretro after testing,
only the developer edits these files, and the code-side lessons from
testing - keep comments true, one path for reset and close content,
upstream's way of stopping threads, no core options that do nothing.
CLAUDE.md only imports AGENTS.md, so there is one copy of the rules.
2026-09-26 08:47:56 +02:00
WizzardSK 69049a592d Say which build wrote a log
Cemu names its build in the "Init" line, which a libretro core writes
before its log.txt exists, so a log sent in never said which commit made
it (sco8487). log.txt now names it under "Loaded title", and the
frontend's log in the first line from retro_init. The hash is the one
CMake takes from git at configure time, as it is for the standalone
build.

(cherry picked from commit e387d972eb8f4a2ae7a27ceb2d2ff992b556ffd8)
2026-09-26 07:56:34 +02:00
WizzardSK 5837c4eb6c A place to put updates and DLC
Neither is content a frontend can hand over: an update mounts over the base
title's /vol/content and a DLC mounts as /vol/aoc, and both are picked out of
the title list by title id rather than by the path that was loaded. The core
scans mlc01 already, but it had no game path at all, so the only way to get an
update in front of it was to build the mlc01 layout by hand.

There is now a directory for it - Cemu/titles in the frontend's system
directory - added as a scan path. That is the same scan Cemu's own game paths
get, which takes an update in the raw NUS form it is downloaded in (.app and
.h3 next to title.tmd and title.tik) as well as an unpacked code/content/meta
folder, and descends into subdirectories, so a folder per update or DLC works.

Nothing is copied: a title update is several gigabytes, and writing it twice to
install it is a cost with nothing behind it.
2026-09-25 21:15:38 +02:00
WizzardSK 51ea3934cb ci: webOS: print the debug notes instead of keeping them as an artifact
The buildbot packages every artifact of a job as a core, so
ci-webos-debug.log went out next to cemu_libretro.so. The notes now go to
the job's trace from after_script, and the only artifact is the core.
2026-09-25 18:19:27 +02:00
WizzardSK a3e93ef19e ci: webOS: build the vcpkg ports position independent
The webOS core now compiles, and stops at the link: SDL3's static
library has a GOT reference built without -fPIC, which cannot go into a
shared object. vcpkg's Linux toolchain adds -fPIC itself, but the webOS
triplet chainloads the SDK's toolchain instead, so nothing did. The
triplet now asks for CMAKE_POSITION_INDEPENDENT_CODE and -fPIC.
2026-09-24 21:41:34 +02:00
WizzardSK 01e59e20b2 ci: webOS: leave GLX out of GLInclude.h
With the link fixed, the webOS build gets into compiling, and stops in
every file that includes GLInclude.h: on BOOST_OS_LINUX it declares the
GLX types from X11's XID, which webOS does not have. It now skips them on
webOS as it already does on Android; nothing else in the GL loader is GLX.
2026-09-24 19:32:01 +02:00
WizzardSK 2ed85cb96f ci: webOS: link neither X11 nor desktop GL
The webOS job now gets through vcpkg and configure, and stops at generate:
CemuCommon links X11::X11 on every UNIX but Apple and Android, and webOS
has no X11 - the top-level find_package(X11) already skips it, so the
target does not exist. CemuCommon now skips webOS too.

The core's own link line had the same shape, GL EGL X11 for every UNIX
but Android. webOS gets none of them: ENABLE_OPENGL is off there, the
EGL/GLX code in CemuLibretro.cpp is all behind it, and Vulkan is loaded at
run time.
2026-09-24 18:32:23 +02:00
WizzardSK 97d79b775b Translate the core options through libretro's Crowdin
The option definitions move into libretro_core_options.h in the layout
libretro's Crowdin scripts read, and libretro_set_core_options there picks
the frontend's language, falling back to v1 or the flat v0 list. Lists
found at run time are still filled into the English definitions before
each publish.

libretro_core_options_intl.h starts with every translation empty, so
nothing changes until the first sync. intl/ and the two workflows upload
the English texts on change and pull the translations back weekly; both
need a CROWDIN_API_KEY repository secret.
2026-09-24 17:09:32 +02:00
WizzardSK 8b93a8a041 Grant the audio of the time not spent waiting for the frontend's audio
sco8487's second log, outdoors in Deus Ex at ~15 fps, shows retro_run at
9-30 a second with the phone's GPU holding each frame up for up to 110 ms,
and the audio at half speed again whenever a gap outgrew the 100 ms cap.
Raising the cap alone is not safe: RetroArch waits in the audio callback
until what it was given fits, and a grant that counts that wait feeds it
back - more audio, a longer wait, a larger grant - which is how the
wall-clock audio drifted to 10 fps.

The grant is now the time since the last retro_run less what went on
waiting in the audio callback, capped at 250 ms. A frame the GPU holds up
still gets its audio; a long wait in the audio callback makes the next grant
smaller, so the frontend's buffer has room again and the wait undoes itself
instead of growing.
2026-09-23 21:32:41 +02:00
WizzardSK 2f089083ed Let a slow device's audio grants add up
sco8487's phone runs Deus Ex at 20-29 retro_run a second, 35-50 ms apart,
and still got only 240-310 of the 333 audio frames a second it needs, and a
little crackle: the grant was capped at 50 ms and the budget at 2400
samples, so a grant 50 ms or more after the last was cut, and whatever the
title had not yet used of the previous one pushed the new one over the
budget's cap and was lost. Both caps are 100 ms now. On a machine that keeps
up a grant is ~17 ms either way.
2026-09-23 21:20:41 +02:00
WizzardSK 9e9f35fa97 List only the graphic packs for the running title
With a full pack download there are some 800, and listing each put hundreds
of lines in log.txt. The count is kept, with how many are for this title,
and those are the ones listed, on or off.
2026-09-23 20:44:42 +02:00
WizzardSK b4a752978a Report the output size from the start, and list graphic packs in log.txt
The frontend asks for the geometry right after retro_load_game, before the
core options are applied in the first retro_run, so it was told 1280x720
whatever the internal resolution was. retro_get_system_av_info now reads the
option itself.

The graphic pack list went to log.txt before the log file is open, and was
lost. It is now written where the title activates its packs, with whether
each is on and whether it is for the running title.
2026-09-23 20:31:50 +02:00
WizzardSK 570b486232 Grant the audio of the real time since the last retro_run
296e8e01 granted a fixed frame's worth (800 samples) per retro_run, which is
right only while retro_run comes 60 times a second. sco8487's phone manages
Deus Ex at 18-25, so it got a third to a half of the audio it needed, and the
title, which follows its audio, slowed down with it. The grant is now the
time since the last retro_run at 48 kHz, capped at three frames' worth, so a
stall cannot grow into a longer wait in the audio callback and a bigger grant
after it - how the wall-clock audio before 296e8e01 settled at 10 fps.

With Vulkan the picture handed to the frontend now follows the internal
resolution option instead of being 1280x720 whatever the title is rendered
at (NNshi): the presentation image, the window the final blit fills, the
geometry in retro_get_system_av_info, video_cb and the touch mapping all take
that size, from when the renderer is made at content load. A change of the
option while running still changes the render scale; the output size follows
at the next load, as the GPU thread draws into the presentation image the
whole time. The OpenGL path reads its frames back into a fixed 1280x720
buffer and stays at that.

log.txt now lists the graphic packs found, whether each is on and for how
many titles, so a pack that is not picked up can be told apart from one that
is found but switched off.
2026-09-23 20:18:05 +02:00
WizzardSK 7ecff40258 Keep retro_run at 60 Hz whatever the title renders at
296e8e01 grants one frame's worth of audio per retro_run, which is right only
while retro_run keeps coming at the rate the core reports. It waited up to
33 ms for the GPU thread's frame, so a 30 fps title - BotW - made retro_run
itself ~30 Hz and the audio played at half speed (NNshi). The wait is now at
most 12 ms: a late 60 fps frame still makes it, and one that does not is
shown by the next retro_run while this one presents the last image again.
2026-09-23 17:18:25 +02:00
WizzardSK 296e8e0101 Pace AX by the frames the frontend asks for
The frame timing says where the quarter frame rate comes from: each retro_run
spent ~100 ms in the audio callback and nothing anywhere else. With the cores
running between frames, AX made audio by the wall clock, and RetroArch blocks
in the audio callback until what it was given has played - so a slow frame
meant more audio for the next one and a longer block in it, until the ring's
6144 frames capped it at ~10 retro_run a second.

In a libretro core the audio has to follow the frames instead. Each retro_run
now grants one frame's worth, 48000 / 60 = 800 samples, and AX queues a 3 ms
frame only when there is budget for it; the wall-clock gate is not used in
the core. The audio callback then paces retro_run at the reported 60 fps, a
paused frontend grants nothing, and the budget is capped at three frames so a
stalled title does not come back with a burst.
2026-09-23 15:13:32 +02:00
WizzardSK 108d1282a2 Measure the Vulkan path too
The Vulkan branch of retro_run presents and returns on its own, so the timing
at the end of the function never ran there. Both of its exits and the OpenGL
one now end through libretro_finish_run, which hands the audio over and
accounts the frame.
2026-09-23 14:58:10 +02:00
WizzardSK dd98a552e0 Say where a frame's time goes
With the cores running between frames the audio is right, but the frame rate
falls to a quarter (NNshi 60 -> 13-15, here 30-50 -> 10), and taking the gate's
mutex off the idle loop did not change it. So measure instead of guessing: per
second, how many retro_run calls there were and how many timed out waiting for
the GPU thread, how many frames the GPU thread made ready, how long retro_run
spent waiting, presenting and handing over audio, how long the frontend took
between calls, and how long the GPU thread waited at the gate for a token.
Logged with cemu_log_audio.
2026-09-23 14:43:21 +02:00
WizzardSK d06c3cbff9 ci: webOS: hand the ports the SDK's pkg-config
With openssl through, the next port is curl, and its configure stops at
"pkg-config tool not found" although the SDK has one on PATH: under the SDK's
toolchain file find_program looks in the target sysroot. The job exports the
path and the triplet passes it as PKG_CONFIG_EXECUTABLE.
2026-09-23 14:40:52 +02:00
WizzardSK 9460b27d0a Read the frame window without taking the gate's mutex
4afd9f53 left the emulated cores running between frames, and with that the
main core's idle loop comes round millions of times a second - taking
s_gate_mutex in libretro_frame_window_wait on every pass. The GPU command
processor takes the same path at every command, so it queued behind the idle
loop for the mutex at each one: NNshi went from 60 fps to 13-15, and this
machine from 30-50 to 10-13, with the audio itself now at the right rate.

The window state becomes atomics, still written under the mutex so the
condition variable cannot miss a grant, and read without it. The mutex is
taken only by a thread that actually has to wait.
2026-09-23 14:24:10 +02:00
WizzardSK ab5c40f356 ci: webOS: unset the kernel variables the SDK exports
environment-setup exports ARCH, CROSS_COMPILE and KERNELDIR for kernel builds.
OpenSSL's Configure prepends CROSS_COMPILE to the compiler the triplet hands
it, which is already an absolute path, so the openssl port failed on every
object with aarch64-webos-linux-gnu-/developer/.../aarch64-webos-linux-gnu-gcc:
not found.
2026-09-23 14:10:40 +02:00
WizzardSK 4afd9f53e5 Keep the emulated cores running between frames
NNshi's log settles where the audio goes: AX update is reached 575 times a
second and its clock gate opens every time, but the title's AX thread renders
60 frames a second - 180 ms of audio per second. Each 3 ms audio frame is a
round trip: the main core asks, the AX thread on core 2 renders it, and only
then is the next one asked for. The frame window parked every emulated core
from the swap until the next retro_run, so that round trip fit once per video
frame.

The window was there so that a paused frontend stops the emulator (#12). That
only needs the cores to stop once retro_run stops coming, not at every swap:
they now park when the last grant is more than 100 ms old. The frames stay
paced by the GPU thread's token, which is unchanged.
2026-09-23 13:35:54 +02:00
WizzardSK e1a73bb539 Separate a core that is idle from a core that is held
NNshi's log has the shape of the answer in it: the guest's AX thread completes
60 frames a second, and the core it runs on comes round 60 times a second -
while core 1 manages 450 and core 0 never runs a guest thread at all. So the
audio rate is the rate that core is scheduled at, one pass per video frame.

What the numbers cannot yet say is why. A core spinning in its idle loop with
nothing runnable and a core parked at the frontend's frame gate look identical
from the fiber count, so the idle-loop count per core goes in beside it: idle
high with fibers low is a guest with nothing to run, both low is the gate.
2026-09-22 23:42:03 +02:00
WizzardSK 5cb582ad43 Measure the guest's side of the audio too
NNshi's new log moves the question again. The gate is not the problem: AX update
is called 380 to 660 times a second and the gate opens every single time - the
two counts are identical on every line. Yet AX produces 17280 samples a second,
which is 60 blocks of 3 ms: exactly one per video frame.

The filter is after the gate. A frame is queued only when

  numQueuedFramesSndGeneric == snd_core::getNumProcessedFrames()

so the next block waits for the title's own AX thread to finish the last one,
and that thread is finishing 60 a second where 3 ms blocks want 333. Whether
that is the guest being scheduled too rarely or something else holding it, the
report now carries the two numbers that say so: frames the guest's AX thread
completed, and how often each emulated core came round its scheduler loop.
2026-09-22 19:55:10 +02:00
WizzardSK 48a483c8f1 Say how often AX is even asked to produce
NNshi's log settles half the question: AX produced 17280 samples in a second
where 48 kHz stereo needs 96000, the ring dropped nothing, the frontend took
everything, and 45 of 60 flushes had nothing to send. Nothing is being lost
between AX and RetroArch - AX is not making it.

Which leaves two causes and no way yet to tell them apart: the scheduler not
reaching __OSCheckSystemEvents often enough, or it reaching AXOut_update and
the 3 ms gate holding it shut. The report now carries all three numbers - the
scheduler's system-event count, how many times AXOut_update was called, and how
many times the gate opened - so one log says which.
2026-09-22 19:21:36 +02:00
WizzardSK 9c5bc40e8d Wake retro_run when a Vulkan frame is ready
NNshi: Fast Racing Neo runs at 30 on Vulkan and 60 on OpenGL, same machine,
same title. retro_run waits for the GPU thread like this:

  std::unique_lock lock(s_frame_mutex);
  s_frame_cv.wait_for(lock, 33ms, [] { return s_frame_ready.load(); });

The OpenGL canvas sets that flag under the mutex and notifies the condition
variable. The Vulkan renderer stored it on its own - no lock, no notify - so
unless the frame happened to be finished before retro_run started waiting,
nothing woke the wait and it ran to the 33 ms timeout. 1000 / 33 is 30, which
is the number he is seeing, and it caps every Vulkan-only target the same way,
Android included.

Both paths now go through one signal that takes the mutex, sets the flag and
notifies.
2026-09-22 18:47:02 +02:00
WizzardSK 102e925b47 Leave a trail through retro_init
jacklavin's core started crashing at core selection, before any content, and
produced no log. That is consistent: Cemu's log.txt does not exist until
retro_init has resolved the paths, so anything that dies earlier than that -
or between the steps after it - leaves nothing to read.

retro_init now reports each step through the frontend's log, which exists from
the moment RetroArch loads the core: start, paths, settings.xml, the graphics
API choice, activating settings, done. A crash then names the step it happened
in rather than being silent.
2026-09-22 18:43:51 +02:00
WizzardSK 49cf1c542a Describe the title's memory to the frontend
sco8487 asked about libretro-style cheat search. The half of it that needs no
guessing at the Wii U memory model is this one: Cemu already keeps the whole
PowerPC address space in a single host allocation, so a window into it is a
pointer and a length, and RetroArch's cheat search and memory viewer work off
exactly that.

SET_MEMORY_MAPS now describes MEM2, MEM1 and FGBUCKET - the ranges that are
actually mapped, rather than the whole 4 GiB, because searching space that was
never mapped is slow and full of false hits. They are flagged big-endian, which
is what makes a search for a value find it, and the addresses the frontend shows
are Wii U effective addresses: the same ones published codes use, and stable
across runs because MEM2 does not move.

The pointers are only good while a title is mounted, so the map is published
after the title launches and an empty one replaces it before ShutdownTitle
unmaps the memory.

retro_cheat_set stays a stub. Applying codes means parsing a format and writing
every frame, which is where the mistakes would be; the memory map already gives
search and poke through the frontend.
2026-09-22 18:39:09 +02:00
WizzardSK 8ae736b600 Say where the audio goes missing
NNshi is seeing a fraction of the samples he should on Windows and the reports
can only say 'too few'. There are three ends to that chain and they need telling
apart: AX not producing, the ring overflowing before retro_run drains it, or the
frontend refusing what it is offered.

cemu_log_audio, off by default, reports once a second what AX produced, what the
ring had to drop, what was drained and what the frontend took, plus how many
flushes found nothing to send and what the ring is holding. 48 kHz stereo is
96000 samples a second, so the first number alone says whether the title is
getting the time to make audio at all.
2026-09-22 18:32:21 +02:00
WizzardSK 094e9d8064 Hide the layout combination for as long as it is held
sco8487: the overlay button still fires the title's own bindings. His reading
was right and the code agrees with it - e7e3334 dropped the frame's input only
where the layout actually changed, which is the rising edge, `down &&
!s_next_layout_button_held`. A press lasts many frames, so the title saw every
frame of the combination except the first: L3, R3, the shoulders and Select all
arrived as usual, one frame late.

The suppression now follows the combination rather than the edge: the flag is
set on every frame it is down, and the layout still steps once per press. Same
flag, same wholesale clearing of the frame - only the condition that raises it
has changed.
2026-09-21 22:22:52 +02:00
WizzardSK 76853737fb Pace AX with the Windows clock only on Windows
29b06ff swapped the AX gate from now_cached to tick_cached to fix a MinGW
build producing a fifth of its audio, and sco8487's bisect then landed on that
commit for repeated samples in Deus Ex dialogue on Android. I had assumed the
change could not reach Android. It can: outside MSVC the two helpers are not
the same clock. now_cached is std::chrono::high_resolution_clock, which
libstdc++ makes system_clock - CLOCK_REALTIME - while tick_cached on
BOOST_OS_LINUX, which Android is, reads CLOCK_MONOTONIC_RAW. Raw is not slewed
to follow real time and is not always served from the vDSO, so the gate went
from a clock that tracks the audio device's notion of a second to one that
deliberately does not.

The Windows problem is real and stays fixed; the clock swap is now confined to
the platform that needed it.
2026-09-21 22:09:57 +02:00
WizzardSK 271fc32c1c Build the core for webOS
aarch64 only. The other half of the platform is armv7a, and the recompiler
backends here are x86-64 and arm64 - there is nothing for a 32-bit ARM device
to run Espresso code on, so that half is not offered rather than offered and
broken.

webOS is a Linux by CMAKE_SYSTEM_NAME whose buildroot is as bare as Android's:
EGL and GLES, no X11, no Wayland, no session bus, no desktop GL. Everything
here follows from that. The X11 headers in platform.h, the Xlib and xcb
surfaces in the Vulkan backend and the VK_USE_PLATFORM_*_KHR defines that make
the Vulkan headers reach for xcb/xcb.h are all skipped the way they already are
on Android, and the desktop session options - Wayland, GameMode, BlueZ - default
off. OpenGL goes with them: the backend here is desktop GL 4.5 with direct
state access, so Vulkan is the one that fits, and it needs nothing from the SDK
because the loader is dlopen()ed and the headers are vendored.

The dependency set is the part with no precedent. vcpkg builds its ports with
the toolchain named by their triplet - the one CMakeLists chainloads reaches
try_compile and nothing else - so arm64-linux would have built all thirty of
them for the host. Hence dependencies/vcpkg_triplets/arm64-webos.cmake, which
points at the SDK's toolchain file, and a webOS overlay carrying only the empty
dbus port: the Linux overlays take libpng, cairo and gtk3 from the system,
which in a cross build means the host's x86_64 copies. libusb is left out for a
reason of its own - its Linux backend wants libudev, which the buildroot does
not carry, and a webOS app has no raw USB access to use it with.

Not built anywhere yet: the SDK exists only on the buildbot, so the job is
allow_failure and keeps a probe log as an artifact - the image has never run
vcpkg, and a trace needs an account to read while an artifact does not. What
the job cannot answer either way is whether a given TV has a libvulkan for the
core to dlopen; where there is none, the core loads and fails at renderer
creation.
2026-09-20 20:43:27 +02:00
WizzardSK e7e33340d8 Keep the layout combination out of the title's hands
sco: the button that steps through screen layouts also reaches the game,
so switching layout fires whatever the title has bound to L3, R3, the
shoulders or Select at the same time.

The frame a combination fires in is now a frame the title sees no input at
all: the GamePad state is cleared after it is built, and the two accessors
the Wii Remotes read through answer empty for that frame as well. A flag
rather than per-button bookkeeping, which is what sco asked for and is
enough here - the combinations are deliberately unusual, so a pad holding
one meant the layout change and nothing else.
2026-09-20 16:00:32 +02:00
WizzardSK 01a804d02a Stop offering a notification setting nothing can show
sco asked whether "On-Screen Notifications" does anything visible. It does
not, and not because of when the shader cache loads: both renderers refuse
the imgui frame the notifications would be drawn in.
OpenGLRenderer::ImguiBegin returns false on its first line under
ENABLE_LIBRETRO, because there is no GL context on the GPU thread, and
VulkanRenderer::ImguiBegin gets there through AcquireNextSwapchainImage,
which fails because a core presenting into an image the frontend owns has
no swapchain. Neither path can put an imgui pixel on the screen.

So the option is gone and the position is held at disabled. Upstream's
overlay code is left exactly as it is - it is shared with the emulator and
only the core's use of it was ever the question.

The comment that went with the option was wrong as well, and it is worth
saying so: it claimed an imgui frame was built every presented frame when
a position was set. LatteOverlay_wantsToDraw() does gate on the position,
but what follows it is an ImguiBegin that returns false, not a render
pass. The cost was one call, not a frame.

The notifications are still worth having - the async shader compile
warning most of all - but through the frontend's own OSD: forwarding
LatteOverlay_pushNotification to SET_MESSAGE_EXT. That is a feature, not a
setting, and it is not in this commit.
2026-09-19 18:19:14 +02:00
WizzardSK 9be7c93703 Put the sRGB encode back into the Vulkan present
NNshi: "the vulkan image is too dark, OGL looks proper". It is, and the
reason is that the two paths do different things.

Upstream presents by drawing the backbuffer quad through
RendererOutputShader, which ends with

    if (applySRGBEncoding) colorOut0 = sRGBEncode(colorOut0.rgb);

where applySRGBEncoding is LatteGPUState.tvBufferUsesSRGB. The OpenGL path
in this core still goes through that shader. The Vulkan path does not: it
replaced the draw with vkCmdBlitImage into the presentation image, and a
blit does no such encode, so the values reach the frontend linear and are
shown uncorrected. That is exactly what a missing sRGB encode looks like.

The image is created as VK_FORMAT_R8G8B8A8_SRGB now whenever upstream
would have encoded, so the blit does the conversion in hardware on the way
in, and as UNORM otherwise. The view handed to the frontend stays UNORM -
the image already carried MUTABLE_FORMAT - because sampling it as sRGB
would decode what the blit just encoded and give back the dark picture.
The scan buffer can change colour space while a title runs, so the blit
checks and rebuilds the image when it does.

This does not bring back the rest of that shader: the bicubic and nearest
filters and the TV/DRC gamma settings are still missing on the Vulkan
path, because a blit only does linear or nearest. Presenting through the
output shader is the fix for those, and it is a bigger change than this
one.
2026-09-19 17:56:59 +02:00
WizzardSK 29b06ff0fa Pace AX with a clock that moves, on Windows too
NNshi's Windows core produced about a fifth of the audio it should, which
is silence on WASAPI and a 90% underrun on XAudio and ASIO. The ratio is
the tell: AXOut_update gates on 3 ms and queues one 3 ms frame per pass,
and 15.6 ms divided by 3 ms is 5.2.

15.6 ms is the Windows system timer period, and that is the resolution of
std::chrono::high_resolution_clock on a MinGW build - libstdc++ makes it
an alias of system_clock, which reads the system time rather than QPC.
now_cached() hands MinGW exactly that, because its fast path is written in
MSVC STL internals and everything else falls through to the standard
clock. Upstream never meets this: its Windows build is MSVC, where
now_cached is QPC by hand, and on Linux and macOS the standard clock is
fine.

So the pacing uses tick_cached() instead, which is QPC on every compiler
and monotonic besides - which is what a delta wanted in the first place.

The same fallback is in VPADController for its input tick. Left alone:
nothing has been reported there and a coarse clock costs less in input
sampling than it does in an audio gate, so that is a change to make with
evidence rather than by analogy.
2026-09-19 17:52:46 +02:00
WizzardSK 71418964e4 Make the options the game profile owns actually hold
Three of them did not. gameProfile_load() runs inside
PrepareForegroundTitle and starts with ResetOptional(), which puts
accurate shader mul, shader fast math and the thread quantum back to their
defaults and then copies the quantum into ppcThreadQuantum. Every option
was applied before the launch, so the profile overwrote all three a moment
later: the option moved the value and the title ran with the default. The
same shape as the audio latency one - the value was set somewhere that
something else owned.

They are applied from the launch now, after the profile has loaded, and
still from libretro_apply_core_options so that a change made while a title
runs takes effect there too, where no profile load follows.

Worth knowing about shader fast math: upstream keeps it as a Metal-only
game profile setting, so on every build except Apple's the option has
nothing to set. It is offered everywhere.

And cemu_skip_draw_on_dupe is gone. It came in with the original core,
where it was a static that nothing read, and nothing has read it since
either: no upstream setting behind it, no code in this fork. An option
that does nothing is worse than no option, and a declared option is one
the frontend writes into its .opt file.
2026-09-19 17:33:20 +02:00
WizzardSK 0edac544f9 Name the stop for what it does now, and put its comment back on it
libretro_shutdown_title_for_exit stopped being only about the exit when
the close moved into context_destroy: an exit, a reset and a graphics
context going away all end in it. It is libretro_stop_title now.

Its explanation had also drifted one function up, onto
libretro_wake_frame_waiters, when that helper was split out - so the
paragraph about flushing save files through ShutdownTitle was sitting
above a function that does neither. Moved back, with the heading saying
which three callers there are.
2026-09-19 17:19:29 +02:00
WizzardSK 1192defc10 Make the audio latency option reach the audio path
cemu_audio_latency was read and then went nowhere: the audio device takes
its delay from IAudioAPI's static default, which only the standalone's
main.cpp ever set. The option sets it now, through SetAudioDelay, and the
device is built with it.
2026-09-19 17:17:32 +02:00
WizzardSK 13c08e3427 Say in Cemu's own log what the graphics context is doing
The context lifecycle was only ever reported through log_cb, which lands
in the frontend's log, and log.txt is what a bug report carries. "Did the
context come back" could not be answered from it - which is exactly the
question an Android log could not settle. So context_reset says it has a
context, context_destroy says when it is going with no title loaded, and
each of the three "a context after a context" branches says so too: those
are the rebuild path, and with cache_context set nothing but a real device
loss or a frontend that could not keep the old context reaches them. If
they never appear in anyone's log, they are dead and can go.

Two comments went out of date with the close moving into context_destroy.
The handshake below it no longer runs "while the title keeps running" -
after the close branch there is never a title - and a fullscreen toggle is
no longer the example for a context coming back, because with
cache_context set a toggle calls neither callback.
2026-09-19 17:17:32 +02:00
WizzardSK f0368bf734 Fold the four repetitions in the libretro layer into one place each
Stopping a title: libretro_shutdown_title_for_exit does the gate wake and
CafeSystem::ShutdownTitle, retro_unload_game called it and then repeated
the same wake immediately afterwards, and context_destroy carried a third
copy inline. The wake is libretro_wake_frame_waiters now, called from both
of the shutdown function's paths - the early one matters, because a GPU
thread can be parked at the frame gate with no title ever having launched.

The pad mappings: libretro_map_wiimote, _pro and _classic were three runs
of near-identical set_mapping calls and are lists of (button, source)
pairs now, applied by libretro_apply_pad_mappings. The mappings themselves
are unchanged, one for one.

The option text: libretro_set_core_variables and
libretro_set_core_options_v2 both split "Description; a|b|c" by hand and
both walked the bar-separated list by hand. libretro_split_option does
both once.

The log lines: 77 "if (log_cb)" guards around 86 calls, each repeating the
"Cemu: " prefix inside its format string. libretro_log checks the pointer
and adds the prefix, and carries the printf format attribute so every call
site is still checked. The sink that forwards Cemu's own log keeps calling
log_cb directly: its lines already carry their own prefix.
2026-09-19 17:17:24 +02:00
WizzardSK bd05b8ac1c Stop the title while there is still a renderer to stop it with
Closing content hung: OSSchedulerEnd joined core 1 and never came back,
with 26 guest threads waiting and one running. The running one was
spinning on the GPU, and the GPU was gone - context_destroy runs before
retro_unload_game and took the renderer with it, so a guest thread that
polls the GPU rather than sleeping on it never reached a yield point and
the join never returned.

Upstream stops the scheduler first and the GPU thread second, and that
order is what lets it finish: a thread waiting on the GPU is still being
served while the scheduler winds down. This core could not use that order
until it knew whether a context_destroy was a close. hunterk confirmed
that with cache_context set - which this core sets on both the GL and the
Vulkan path - nothing but unloading content, exiting and a real device
loss calls it, and all three end the title anyway.

So the title goes down at the top of context_destroy, in upstream's order,
while the renderer is still there. Verified on Android: core 1 joins in
under a millisecond, the whole ShutdownTitle takes 470 ms, and the pause
handshake afterwards finds nothing left to park.
2026-09-19 17:17:15 +02:00
WizzardSK 0c53879d62 Do not hand the frontend an image nothing has drawn into
A frame the GPU thread never finished is a buffer with whatever was in it
before, and presenting it showed that: one flash of the last title, or of
nothing, whenever a frame missed its deadline. A frame that was not drawn
is now a duplicate instead, which is what the frontend expects from a core
that did not produce one.
2026-09-19 17:17:08 +02:00
WizzardSK 3b99de0faf Let AX size the audio device, because it is the one filling it
The device was created with a block size the core chose and AX then wrote
blocks of its own size into it, so the ring was being filled in units it
was not built for. AX knows how much it produces per tick; it says so now
and the device is built to match.
2026-09-19 17:17:08 +02:00
WizzardSK 4bf39abc21 Start and reset the title on the thread that asked for it
The title used to start on a thread of its own, which left a window the
frontend could close content in: a launch still building its state while
ShutdownTitle tore that state down, ending in a fault on a fiber whose
stack had just been freed. It starts inside retro_run now and that call
does not return until the title is running or has failed, so while the
core is launching, the frontend is inside the core and cannot be anywhere
else.

A reset is the same problem twice, because RetroArch calls retro_reset
from its menu task: both halves of it - the stop and the start - belong to
retro_run's thread now, whatever thread asked. retro_reset sets a flag and
nothing more, and a reset that arrives twice before the next frame is one
reset. A reset that failed used to leave the frame gate open for good;
that is closed too.

The two diagnostics here are what made these findable and they stay:
OSSchedulerEnd says which core it is joining and what that core was doing,
and a core that will not stop names the guest thread still on it.
2026-09-19 17:16:59 +02:00
WizzardSK 96ee059b3e A switch for the log the controller reports need
Issue #24 asks for a Wii Remote in titles that will not see one, and the answer
in every case is what the title asked padscore for and what it was told. That
is LogType::InputAPI, and nothing here could turn it on - so the report that
would settle it was one nobody could produce, including the one I asked for in
that issue.

Its own option rather than part of the system API one: WPADRead and KPADRead
are logged per call, so this is a line per channel per frame. That is fine for
the few seconds it takes to see whether a title probed at all, and not fine
left on.
2026-09-17 12:20:20 +02:00
WizzardSK 575236a302 Build the core for iOS and tvOS
Nine rounds of compiling it, and both platforms now produce a core: the tvOS
one reports platform TVOS, minos 13.4, sdk 18.5, and links nothing outside
/usr/lib.

The two jobs pick what the platform decides - no OpenGL (the emulator's GL
backend has nothing to run on where GL means EAGL and GLES), no Vulkan
(MoltenVK is a dylib and these systems will not load a loose one), so the GS is
Metal, with the readback present path the core already has for frontends that
hand out no hardware context. vcpkg has arm64-ios and arm64-tvos triplets of
its own, so the dependency set needed no new machinery, only libusb left out
(its Darwin backend is IOKit's USB family, and the SDK has no IOCFBundle.h at
all) and openssl pinned to a version whose port knows tvOS.

Both jobs are allow_failure while they settle, like the macOS ones. Nothing has
been run on a device - JIT there needs an entitlement a stock install does not
grant, which is the same bargain every JIT core on these platforms makes.
2026-09-16 23:47:17 +02:00
WizzardSK d3a5ab3c77 Three pieces of the glue that assumed Vulkan, or a desktop
Round eight is inside the libretro glue itself, which is progress of a kind.

LibretroVkQueue.cpp was compiled unconditionally and includes
libretro_vulkan.h, which includes vulkan/vulkan.h - a header a build without
Vulkan has no reason to have. It moves in with the rest of the Vulkan pieces.

The cemu_bc1_16bit option sets g_libretroNarrowBC1, which is declared in the
Vulkan renderer's header, so reading that option is Vulkan's business too.

And _putenv in main.cpp, which is the boost.predef story again: the macro is
defined for BOOST_OS_MACOS and BOOST_OS_BSD, neither of which an iOS build
answers to, so it had no _putenv at all.
2026-09-16 23:33:22 +02:00