Commit Graph
6022 Commits
Author SHA1 Message Date
Henrik Rydgård 79cedbc13f Merge pull request #22734 from hrydgard/vulkan-stale-extent-workaround
Android Vulkan: Bring back the stale surface extent workaround
2026-10-22 21:52:20 +02:00
Henrik RydgårdandClaude Opus 5.5 f8e7efc8b0 Android Vulkan: Check the surface extent against the latest window size
g_display lags behind surfaceChanged until the render thread applies the
resize, and a recreate after a suboptimal present can run in between.
The check then swapped a correct extent for a frame. Android now sets the
draw size callback to the latest size from backbufferResize, and the
check uses that.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 20:49:08 +02:00
Henrik RydgårdandClaude Opus 5.5 82e6ac86f0 Vulkan: Balance the debug label of a swapchain pass that has no image
A swapchain pass closes its step label and opens one in presentCmd, but
only if it gets an image. With the main window minimized it doesn't, and
the end of the step closed a label that wasn't open, in presentCmd if an
extra swapchain's pass had moved recording there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 20:41:35 +02:00
Henrik RydgårdandClaude Opus 5.5 ea5e39c440 Android Vulkan: Bring back the stale surface extent workaround
e32e558aab dropped it, assuming the stale extent came from our own resize
race. A Redmi 9A (PowerVR, Android 11) shows it doesn't: after a rotation,
vkGetPhysicalDeviceSurfaceCapabilitiesKHR returns the new transform with the
old extent, even when queried after surfaceChanged and ApplyResize have
given us the new size, and the wrong swapchain then stays until the next
rotation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 20:29:21 +02:00
Henrik RydgårdandClaude Opus 5.5 8e8cf5dbb4 Vulkan: Multiple swapchains
Swapchains other than the main one are VKRSwapchains, owned by the
render manager, each with its own surface, image views, framebuffers
and depth buffer, in the main swapchain's format so the backbuffer render
pass and pipelines work for all of them. A render step with no
framebuffer can name one.

They don't pace the frame: acquiring doesn't wait, and the pass is
skipped if there's no image. The frame's last submit waits on and
signals the semaphores of every swapchain acquired that frame, and each
is presented after the main one. When one goes out of date, it's
recreated at the start of the next frame.

The main swapchain's path is unchanged; frametests on Vulkan match.
VulkanContext::CreateSurface is the old per-platform surface code, now
also used for the extra ones.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 20:11:36 +02:00
Henrik Rydgård 47e67cb122 Merge pull request #22728 from hrydgard/multi-swapchain-3
Multi-swapchain (multi-window rendering) support, for Metal
2026-10-22 20:09:32 +02:00
Henrik RydgårdandClaude Opus 5.5 b46e386f12 Vulkan: Enable the portability subset features MoltenVK has
We queried VkPhysicalDevicePortabilitySubsetFeaturesKHR but never enabled
it at device creation, so to the validation layers we were using image
view swizzles (and sampler LOD bias) without having them. Enable whatever
the device offers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 18:46:43 +02:00
Henrik RydgårdandClaude Opus 5.5 db6cc41b9d Vulkan: Ask for portability drivers on Mac
Through the Khronos loader (like the Vulkan SDK's, needed for the
validation layers), MoltenVK is only listed if the instance enables
VK_KHR_portability_enumeration. Without it, instance creation failed with
VK_ERROR_INCOMPATIBLE_DRIVER and we fell back to OpenGL. Loading MoltenVK
directly, as the app normally does, works as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 18:46:43 +02:00
Henrik RydgårdandClaude Opus 5.5 3b1432d851 Outputs: Begin the UI frame after binding, and scissor to the output
UIContext::BeginFrame sets the scissor, which on Vulkan needs a bound
render target, and sets it to the primary's size. In portrait, that cut
the TV's menu message off past the phone's width.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 18:42:53 +02:00
Henrik RydgårdandClaude Opus 5.5 014f6dceea Outputs: Name the device in the menu message
"In menus on Henrik's iPhone" if the platform gives us the name the user
gave the device, otherwise "In menus on your iPhone" (or Mac). iOS only
gives the name with an entitlement Apple has to grant, so for now it's
the model there.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 18:42:53 +02:00
Henrik RydgårdandClaude Opus 5.5 3ca76c674c Outputs: Say the menus are on the device, instead of showing black
When no screen on the stack shows anything on an output (we're in the
menus), the background screen draws "Menus are on your device" there.
UI drawn to an output gets the primary's DPI scale, over the whole output.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 18:42:53 +02:00
Henrik RydgårdandClaude Opus 5.5 d491fc90cf Secondary outputs, and --game-window on Mac to show the game in a second window
The ScreenManager keeps a list of secondary outputs, each with its own
swapchain, recreated after a lost device. Platform code adds, resizes and
removes them from any thread through Native_AddOutput() and friends, which
take effect at the start of the next frame.

Each frame, every output is drawn by the topmost screen that claims it
(Screen::renderOutput). EmuScreen claims Game outputs: it presents the
game there, laid out for that output, and leaves it out of the backbuffer,
where the touch controls and the menus stay. Not in non-buffered mode,
which draws straight into the backbuffer.

--game-window opens such an output on Mac, with Metal. It's a smoke test
for multiple swapchains, and a stand-in for an AirPlay display.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 18:42:53 +02:00
Henrik RydgårdandClaude Opus 5.5 56c76ace35 thin3d: Swapchain objects, and multiple swapchains on Metal
The backbuffer is now a Swapchain, from GetPrimarySwapchain(), and
callers bind, measure and read it back through that instead of nullptr.
nullptr still means it too. Backends with one swapchain map the primary
to their old backbuffer path.

On Metal, CreateSwapchain() adds more, each with its own layer, depth
buffer and drawable. Passes merge per swapchain, and Present() presents
every drawable acquired that frame. The backbuffer is just the first
of them. Nothing creates a second one yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 18:42:53 +02:00
Henrik RydgårdandClaude Opus 5.5 5d8db2a22d UI: Give views a pointer to their parent group
Set by ViewGroup when a view is added, inserted or swapped in. Lets a view
find the scroll view it's in, for example.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 15:34:51 +02:00
Henrik RydgårdandClaude Opus 5.5 ab2328b53d Net: Stricter inet_pton, reject empty labels in direct DNS lookups
- inet_pton's IPv6 half is rewritten as a plain parser of the groups before
  and after "::". It no longer rejects a valid trailing "::" after seven
  groups or accepts nine groups around a "::", writes bytes in network order
  without assuming the host's, and only touches the output on success.
- The IPv4 half rejects empty octets ("1..2.3"), and an unsupported family
  returns -1 like the POSIX one, instead of success.
- An empty label ("a..b", ".example.com") ended the encoded name early, so
  the direct lookup asked for something else and cached it under the name
  that was asked for.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 11:51:17 +02:00
Henrik RydgårdandClaude Opus 5.5 66fb9f7ab4 Net: Harden the direct DNS lookup and inet_pton
- inet_pton could write outside its output for IPv6 strings with more than
  eight colons or two "::". Only reachable through AF_INET today, but it's a
  public function. Added a unit test.
- The direct DNS lookup took any datagram that arrived within its timeout as
  the answer, and cached it until restart. The socket is now connected, so the
  OS drops packets from anyone else, the query has a random ID, and replies
  with a different ID, without the response bit, truncated or with an error
  code are rejected.
- Packet fields are read and written with memcpy instead of through unaligned
  pointers, which can fault on 32-bit ARM.
- The lookup cache has a lock. Only the emu thread uses it so far.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 11:45:43 +02:00
Henrik Rydgård 55f10e5590 Merge pull request #22712 from hrydgard/mapped-volume-keys
Input: Let mapped volume keys reach the menus, and consume them in game
2026-10-22 11:37:47 +02:00
Henrik Rydgård 4932e0466d Merge pull request #22721 from hrydgard/settings-bugfixes
Assorted UI bugfixes
2026-10-22 11:31:42 +02:00
Henrik Rydgård b1611a3b47 Merge pull request #22719 from hrydgard/text-measure-cache
TextDrawer: Cache wrapped text, hash the string caches
2026-10-22 11:31:07 +02:00
Henrik RydgårdandClaude Opus 5.5 0c8c447284 Settings: Fix a blank page when reopening settings on the Search tab
The saved tab was selected before the Search tab was added, so the index
was out of range and no tab was created. Select it after adding Search, and
create the current tab anyway if the saved one no longer exists (Search is
hidden on small screens).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 10:55:30 +02:00
Henrik Rydgård 390862a396 Temporary debug mode buildfix for name collision in aemu_postoffice 2026-10-22 10:54:12 +02:00
Henrik RydgårdandClaude Opus 5.5 02b688e4f3 TextDrawer: Cache wrapped text, hash the string caches
MeasureString was slow mostly because of wrapping: DrawStringRect and
MeasureStringRect re-ran the word wrapper every frame, and it measures
every prefix of every word. Wrapped strings are now cached. The caches
are unordered_maps with an xxhash of the key, and lookups reuse a key
buffer so a hit doesn't allocate. UIContext also skips switching fonts
when measuring in the current style. In a Mac debug build, 200 cached
measures go from 0.26 to 0.06 ms and 10 wrapped paragraphs from 2.1 to
0.01 ms per frame.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 10:11:42 +02:00
Henrik RydgårdandClaude Opus 5.5 c51bcd458c Input: Let the key mapping dialog see volume keys, mapped or not
The volume key filter in NativeKey left unmapped volume keys to the OS, so
they never reached the dialog and couldn't be mapped. A new InputMode::AllKeys
lets a screen ask for them; the dialog consumes them so the volume doesn't
change while mapping.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 00:01:34 +02:00
Henrik Rydgård 1adf3c553d Merge pull request #22549 from Kethen/aemu_postoffice_enet
UDP(ENet) mode for adhocctl & aemu_postoffice relay
2026-10-21 23:51:28 +02:00
Katharine Chui 880158cfdb initial enet mode for adhocctl & aemu_postoffice relay 2026-10-21 14:19:54 +02:00
Henrik RydgårdandClaude Opus 5.5 cdf9d38dca iOS: Feed the gyroscope into tilt input
Same as Android: rotation rates go to NativeGyroscope, and SYSPROP_HAS_GYRO shows the "Use gyroscope" option.

Accelerometer data is now scaled from g to m/s^2, like Android's. Tilt normalizes it, but the gyro fusion only trusts the accelerometer when it reads about 9.81, so in g it would never correct drift.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 14:00:33 +02:00
Henrik RydgårdandClaude Opus 5.5 fbf0cc2df4 Android: Detect the accelerometer and gyro instead of assuming them
SYSPROP_HAS_ACCELEROMETER was true for every phone and tablet, whether or
not it had the sensor. The activity now tells the native side what the
sensor manager found, which also answers the new SYSPROP_HAS_GYRO. VR
headsets still report neither. Every other platform returns false for the
gyro for now.

The "Use gyroscope" checkbox uses the new property instead of waiting for
gyro data to show up.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 12:20:20 +02:00
Henrik RydgårdandClaude Opus 5.5 f84e63f753 Tilt: Steady the tilt input with the gyroscope where there is one
Tilt used the raw accelerometer as the gravity vector, which jitters with
sensor noise and jumps when the device is shaken. Now, when gyro data is
coming in, a complementary filter integrates the gyro rates and only pulls
the result toward the accelerometer with a 0.5 s time constant, trusting
the accelerometer less the further it reads from 1g. The fused vector goes
through the same ProcessTilt path, so calibration and sensitivity work as
before. Without a gyro, nothing changes.

The fusion lives in TiltEventProcessor behind NativeGyroscope(), so other
sources (DualSense) can feed it too. Android registers TYPE_GYROSCOPE next
to the accelerometer. A "Use gyroscope" checkbox (on by default, per game)
appears in tilt setup once gyro data has been seen.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 10:45:37 +02:00
Henrik Rydgård a51fe51055 Merge pull request #22690 from hrydgard/me-core
Media Engine part 1: Address space and core (off by default)
2026-10-21 08:37:22 +02:00
Henrik RydgårdandClaude Opus 5.5 8e2bb1faed 32-bit: Keep the memory view range reserved, and mask guest addresses to it
Find4GBBase found 256 MB of free address space for the views and then
unmapped it, so the gaps between the views went back to the system. On a
32-bit Android device, a running game had malloc heaps between VRAM and RAM
and GPU driver buffers just past RAM, and masked guest addresses (bits 30-31
cleared) reach a whole GB above base, mostly heap. A stray guest access
landed in those silently instead of faulting, and another thread could even
map something where a view was about to go, which the view's MAP_FIXED then
replaced. That fits the 32-bit-only crashes deep in system libraries and in
our own heap structures.

Keep the range reserved as PROT_NONE and map the views over it, putting the
reservation back when a view is released. Mask out bits 28-29 too on 32-bit
(no valid address has them), so accesses stay inside the reservation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 23:21:11 +02:00
Henrik RydgårdandClaude Opus 5.5 41851007a2 Memory: Give the Media Engine an address space of its own
The ME sees the main CPU's RAM, but has its own eDRAM at 0 (with
0x80000000 and 0xA0000000 mirrors) where the main CPU has its scratchpad,
its own memory where the main CPU has VRAM, and both CPUs share the boot
page at 0xBFC00000.

- The ME's own memory is views in the main arena flagged MV_ME_ONLY, which
  only Memory::CreateMESpace() maps, into a second reservation along with
  the shared views at their same arena positions. It gets its own block
  shadow.
- The eDRAM (MV_EDRAM) is the ME's half of the eDRAM chip, the GE's VRAM
  being the other: 2MB on the PSP-1000 (whose resident ME image says so),
  4MB on the others (from mcidclan's libmecore, not hardware-checked).
- At VRAM's addresses, through 0x44000000 and 0xA4000000 alike, the ME
  doesn't see VRAM but memory of its own (pspautotest me/vram, on a
  PSP-1000): where homebrew keeps the VME's buffers. Its size isn't known;
  2MB with VRAM's mirrors here, and no depth layout on 16-bit accesses.
- 0xA0000000 reaches physical memory uncached, so RAM is mapped at
  0xA8000000 too.
- The boot page is a view too, which the main CPU reaches through a new
  MMIO device (in savestates from MMIO section version 2). The ME runs
  code from it, uncached, so the main CPU's writes invalidate code there.
  On masked platforms it isn't mapped, and the device uses a plain
  buffer.
- AddressSpace has IsValidAddress etc, with the ME's rules when edramSize
  is set, and the interpreters and the IR interpreter check and fetch
  instructions through their state's space. The IR compiler's checks still
  use the main CPU's, until the ME gets a JIT.
- On Darwin, MemArena can hold several view reservations.

Masked platforms (iOS, 32-bit) don't get an ME space yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 22:22:40 +02:00
Henrik RydgårdandClaude Opus 5.5 135e752105 Windows: Let the language ini's Font override use system fonts again
- Fixes #22675

Since 3aa4714ce0 desktop Windows draws text with DirectWrite, which looked
up every font in a collection of only our bundled fonts, so an override
like "Font = 微软雅黑" silently fell back to Roboto. Fall back to the
system font collection when the family isn't one of ours.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 16:40:37 +02:00
Henrik Rydgård aba3a99888 Merge pull request #22679 from hrydgard/exit-on-game-exit
Android frontends: --pause-menu-exit exits when the game ends, Args extra with URIs, Copy deep link
2026-10-20 13:25:53 +02:00
Henrik RydgårdandClaude Opus 5.5 55f02684f5 Android: Enable "Copy deep link" on the game screen, copying the content URI
PPSSPP has access to the URI, so a frontend or adb can launch the game with it,
which isn't true of a plain path on modern Android.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 12:42:07 +02:00
Henrik Rydgård 58f2eae3b4 Merge pull request #22677 from hrydgard/texreplace-fake-mip
Texture replacement fixes: fake mipmap levels, case-insensitive packs, cache sizing
2026-10-20 12:35:41 +02:00
Henrik RydgårdandClaude Opus 5.5 df40740ae0 ZipFileReader: Lock the archive when looking up a file
Texture replacement looks up files from several IO threads at once, while another thread may be
reading from the same archive. libzip isn't thread safe, and a name lookup writes to the archive: it
caches each name's guessed encoding, and allocates a converted copy of CP437 names.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 11:22:31 +02:00
Henrik RydgårdandClaude Opus 5.5 5cc5a2d802 Replacement: Find pack files whose names differ in case
- Fixes #18746

Packs are mostly made on Windows, where "portraits/x.png" in textures.ini finds Portraits/x.png. On a
case sensitive file system (Steam Deck, Linux) it didn't, while zipped packs already ignored case.

DirectoryReader gets an opt-in ignoreCase mode, used for texture packs. On the first miss it indexes
the directory by lowercased path and retries there, so packs where every name matches never pay for
it, and the walk happens on the IO thread that loads the texture.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 11:22:31 +02:00
Henrik RydgårdandClaude Opus 5.5 7b9e1a1c93 ZipFileReader: Look up the exact name before the case insensitive one
The case insensitive lookup compares against every name in the zip, while an exact one uses libzip's
hash table. Texture packs can have tens of thousands of files.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 11:22:31 +02:00
Henrik RydgårdandClaude Opus 5.5 5a9cbf007c CPU: Pass the MIPSState explicitly where it was already at hand
The JIT backends' calls out of generated code (the MIPS and IR interpreter
fallbacks, and the bad address report) now take the state the backend was
built for instead of reading currentMIPS. Likewise the interpreter's tick
check, the IR dispatcher's Advance, and a couple of debugger helpers.

MIPSState::InitRegisters() no longer sets currentMIPS; the boot paths set it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-20 09:47:48 +02:00
Henrik RydgårdandClaude Opus 5.5 3cc61d3f8e SaveState: Report failed writes, and call back on network refusals
- A failed final flush (a full disk) or a failed rename into place was
  reported as "Saved State". A failed load undo rename still pointed load
  undo at the current game, while the file was another game's state.
- Refusing a save or load for being online didn't call the callback,
  unlike every other refusal.
- A rewind dropped after its decode started stayed active, and stopped
  rewind snapshots until the next clear.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 20:18:46 +02:00
Henrik Rydgård 3ac908345c Merge pull request #22667 from hrydgard/async-savestates
Async savestates: compress and write on a worker thread
2026-10-19 19:25:24 +02:00
Henrik Rydgård e2e0e0c748 Merge pull request #22666 from hrydgard/savestate-fixes
Savestate fixes: rewind ring, slot operations, load failure messages
2026-10-19 18:46:20 +02:00
Henrik RydgårdandClaude Opus 5.5 5d8566ee1e Serializer: Stream zstd when saving and loading state files
Saving compressed into a ZSTD_compressBound() buffer, about the size of
the whole state, and loading read the whole compressed file before
decompressing it. Now saving compresses into a buffer that grows with its
output and frees the uncompressed state before writing, and loading
decompresses the file in chunks. The file format doesn't change.

Saving also compresses before opening the file, which truncates it, and
drops the unused snappy path. LoadFile and SaveFile are public, for doing
the file part away from the state itself.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 18:43:27 +02:00
Henrik RydgårdandClaude Opus 5.5 b75f487dc5 SaveState: Report a state that fails to load as being from another version
When the file reads fine but a section doesn't, that's almost always a
state from another version, so say that (LoadStateWrongVersion), with the
version that saved it and the failing section on the second line.

LoadPtr() now returns just the section title, for callers to phrase.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 17:50:48 +02:00
Henrik RydgårdandClaude Opus 5.5 e3916c0acc SaveState: Fix rewind ring and savestate slot bugs
Rewind:
- first_ never wrapped, so after about 40 snapshots rewinding past the
  oldest state jumped back to the newest ones. Track a count instead.
- States whose base had been replaced stayed in the ring, and each one
  failed a rewind with an empty error. Drop them when the base goes.
- Rewinding after pausing for longer than the snapshot interval restored a
  snapshot taken just before the rewind ran.
- Rewind snapshots (and verification) counted as saves for IsStale().

Slots:
- Loading an empty slot replaced the load undo backup before failing.
- A refused save (hardcore mode) rotated the slot's screenshots first.
- Refused or dropped operations never called their callback, so auto-load
  didn't notice it had failed.
- The slot listing went stale after a save until something rescanned.

Also: Verify always failed (its write pass never set the buffer start),
and corrupt or truncated files were reported as being from an older
version.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 17:29:01 +02:00
Henrik RydgårdandClaude Opus 5.5 6163bf5911 SaveState: Show which PPSSPP version saved a state when loading it fails
The version comes from the file header and goes in the failure message's
metadata line. A state from another version is the usual reason a load
fails. Headless prints it too.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 16:28:47 +02:00
Henrik RydgårdandClaude Opus 5.5 9ddfcca670 Firmware install: Fewer and parallel file operations into the staging directory
Through Android's storage framework, every exists check, create and open is a round trip to the
storage provider, and writing each file took four of them (a check of its directory, a check of
the file, the create, the open). That made a 6.61 install take around 30 s on a phone, against a
fraction of a second of decrypting and inflating.

The install unpacks into a staging directory it has just cleared, so nothing in it can exist yet.
With the new outputDirIsEmpty option, each directory is created once and each file is created and
opened without checking for it first (File::WriteDataToNewFile), on the IO threads while decoding
goes on. A name that turns up twice still goes the careful way, overwriting the first. The swap
into place at the end is unchanged.

Headless --unpack-updater uses the same path when the output directory doesn't exist yet. Its
output is identical either way. With SIMULATE_SLOW_IO, unpacking the fonts went from 17 s to 7.5 s,
most of what's left being the fixed cost of opening the updater.

The UI's install already runs on a thread pool IO thread, so writes queued on the same pool
could stall behind other long IO tasks, and the pool drops queued tasks unrun at teardown,
which would have left the install waiting forever on its in-flight count. Eight threads owned by
the writer (attached for JNI) avoid both, and headless no longer has to start the pool for it.

Also compare file names case-insensitively when looking for repeats, since the storage usually
is case-insensitive and a second create there makes a renamed copy.

Checked with ThreadSanitizer, and on a read-only destination (every file fails, no hang).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 16:06:47 +02:00
Henrik Rydgård cbbfb4a528 Merge pull request #22661 from hrydgard/ios-timer
Better monotonic clock on Apple platforms, fix the generic time fallback
2026-10-19 14:49:01 +02:00
Henrik Rydgård f8bc2d9095 Merge pull request #22660 from hrydgard/ui-fixes
UI: Fix input-thread races on held keys, wheel release and the F9 hack
2026-10-19 14:18:57 +02:00
Henrik RydgårdandClaude Opus 5.5 19b25a2e42 Common: Use CLOCK_UPTIME_RAW for the monotonic clock on Apple platforms
- Fixes #16823

CLOCK_UPTIME_RAW is mach_absolute_time() in nanoseconds (the same value, not
just the same rate). That's the clock MoltenVK reports VK_GOOGLE_display_timing
present times in, which time_now_raw() is meant to match. Apple's
CLOCK_MONOTONIC counts time asleep, unlike Linux's, so on a Mac that had
slept since boot, from_time_raw() of a present time was off by all of it.

It's also cheaper to read: ~19ns vs ~30ns on M1. mach_absolute_time() itself
is ~8ns, but returns ticks, and the timebase on Apple Silicon is 125/3.

Timing risk: time_now_d() no longer advances while the machine sleeps, so a
sleep/wake shows up as a small delta rather than a huge one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 14:17:52 +02:00