Commit Graph
24556 Commits
Author SHA1 Message Date
Brandon 5d346a77ee iOS: Preserve Aspect Ratio When Applying Device Presets
# iOS: Preserve Aspect Ratio When Applying Device Presets

## Summary

This pull request removes Aspect Ratio from the built-in Device Presets under:

```text
Settings > Settings Presets > Device Presets
```

Applying Default, Ultra Quality, High Quality, High Quality 30 FPS, Performance, or Ultra Performance now preserves the Aspect Ratio already selected by the user.

## Problem

The built-in quality and performance presets previously treated Aspect Ratio as part of their managed configuration:

- Default changed Aspect Ratio to Auto.
- The quality and performance presets changed Aspect Ratio to Stretch to Window.

Aspect Ratio is a display preference rather than a performance tier. Applying a graphics or emulation preset could therefore unexpectedly stretch or reshape the image, even when the user wanted to keep a native, automatic, 4:3, 16:9, or custom display choice.

Aspect Ratio was also included in the preset-active comparison. If a user changed only Aspect Ratio after applying a Device Preset, the preset would stop displaying as active even though every setting that the preset should manage still matched.

## Changes

### Device Preset application

Removed the assignment that wrote the built-in preset configuration into:

```swift
settings.aspectRatio
```

Applying any built-in Device Preset now leaves the current Aspect Ratio unchanged.

### Active-preset detection

Removed Aspect Ratio from `BuiltInSettingsPreset.isActive` matching.

The selected indicator now reflects only the settings actually managed by Device Presets. A user can change Aspect Ratio independently without making an otherwise matching Device Preset appear inactive.

### Preset configuration model

Removed the unused `aspectRatio` field and its preset-specific values from the private built-in `Configuration` structure.

This prevents Aspect Ratio from being accidentally restored to Device Preset application in a later refactor and keeps the configuration model aligned with actual behavior.

### User-facing descriptions

Updated the detailed descriptions for Default and Ultra Quality:

- Default no longer states that it restores Aspect Ratio to Auto.
- Ultra Quality no longer states that it changes Aspect Ratio to Stretch to Window.

The Device Presets footer can therefore continue to state that presets change only the listed settings.

## Preserved behavior

This change is intentionally limited to the built-in Device Presets.

The following behavior is unchanged:

- Manual Aspect Ratio selection in Graphics settings.
- Per-game Aspect Ratio overrides.
- Exporting Aspect Ratio in user-created `.ini` preset files.
- Importing Aspect Ratio from user-created `.ini` preset files.
- Internal Resolution settings in all Device Presets.
- FXAA and CAS Sharpening settings.
- Queue Size settings.
- Fast Boot, PNACH, widescreen patch, Fast CDVD, OPH Flag Hack, and Emulation-Only Mode settings.
- Background visibility and Virtual Pad skin behavior.

## Result by preset

| Device Preset | Previous Aspect Ratio behavior | New behavior |
| --- | --- | --- |
| Default | Forced Auto | Preserves current value |
| Ultra Quality | Forced Stretch to Window | Preserves current value |
| High Quality | Inherited Stretch to Window | Preserves current value |
| High Quality 30 FPS | Inherited Stretch to Window | Preserves current value |
| Performance | Inherited Stretch to Window | Preserves current value |
| Ultra Performance | Inherited Stretch to Window | Preserves current value |

## Performance impact

There is no runtime emulation performance cost. Preset application performs one fewer setting assignment and active-preset detection performs one fewer comparison.

The change does not affect the renderer, emulation threads, game boot, frame pacing, or gameplay resource usage.

## Changed file

- `platforms/ios/app/src/main/swift/Models/SettingsPresetCatalog.swift`

## Integration

The worktree was rebased onto the latest `origin/master` before this change was applied.

Base revision:

```text
daed4ed4a7
```

## Validation

- Confirmed that `SettingsPresetCatalog.swift` no longer references Aspect Ratio.
- Confirmed that custom preset import/export support remains present in `SettingsPresetFile.swift`.
- `git diff --check` passed for the changed preset catalog.
- Full unsigned iOS IPA build completed successfully with `build-ios-ipa.sh`.
iOSv2.5.1
2026-07-31 17:09:25 +02:00
Brandon daed4ed4a7 iOS: PS5-Style Core Haptics Rumble Synthesis for iPhone's Taptic Engine
# iOS: PS5-Style Core Haptics Rumble Synthesis for iPhone's Taptic Engine

## Summary

This pull request improves the iPhone rumble fallback by translating live PlayStation 2 large- and small-motor commands into a layered Core Haptics experience.

The previous implementation already maintained two continuous haptic players, but motor changes were applied immediately and a zero value stopped each player immediately. That made vibration starts feel abrupt, stops feel unnaturally clipped, and transitions between the two motor characters feel mechanical.

The new implementation retains the existing controller and SDL rumble routing while improving only the iPhone Taptic Engine synthesis layer. It adds attack and release envelopes, perceptual heavy/light crossfades, transient onset taps, safer delayed teardown, and automatic recovery after Core Haptics interruptions.

## User-facing behavior

### Ramp-up

Starting rumble no longer jumps directly from silence to the requested strength. Each continuous channel begins silently and follows a short intensity curve:

- Large/heavy motor attack: 65 ms
- Small/light motor attack: 28 ms

The heavy motor consequently feels slower and weightier, while the small motor reacts more quickly.

### Fade-down

When a game requests zero rumble, the Taptic Engine now follows a decay envelope before its player is released:

- Large/heavy motor release: 120 ms
- Small/light motor release: 60 ms

This reproduces the perceived momentum of a physical eccentric rotating-mass motor instead of ending the sensation abruptly.

### Heavy/light texture crossfading

The two PS2 vibration motors are represented by separate continuous Core Haptics layers:

| PS2 input | Core Haptics representation | Intended sensation |
| --- | --- | --- |
| Large analog motor | 0.18 sharpness with analog intensity | Deep, rounded, weighty rumble |
| Small binary motor | 0.82 sharpness at a fixed on-level | Crisp, fast, mechanical vibration |

Intensity changes are interpolated over a 45 ms crossfade. Both layers can remain active simultaneously, allowing a game to move between heavy and crisp vibration without flattening both motors into one value.

An iPhone contains one Taptic Engine, so this is a perceptual texture crossfade rather than physical left-to-right motor panning.

### Transient taps and pulsing effects

A rising motor edge additionally emits a short `CHHapticEventTypeHapticTransient` event. Its intensity and sharpness are calculated from the motor combination that started:

- Heavy events produce a softer, deeper onset.
- Small-motor events produce a crisper onset.
- Combined events interpolate between those characters.

Transient generation is debounced to 25 ms. This preserves rapid taps, gunshots, impacts, heartbeat-style rhythms, and game-authored pulsing without creating an event every emulation frame.

The original game remains responsible for the rhythm. The iOS layer does not invent a recurring timer or reinterpret gameplay state.

## Technical implementation

### Persistent real-time synthesis

The implementation continues to use two looping `CHHapticAdvancedPatternPlayer` instances. Each player has a base event intensity of 1.0 because `HapticIntensityControl` multiplies the event intensity.

Each player now begins with an intensity-control value of 0.0. This prevents a full-strength click from escaping between player startup and delivery of the first requested motor level.

Runtime motor changes use `CHHapticParameterCurve` rather than abrupt `sendParameters` updates. The curve begins at the estimated current envelope level and ends at the new target. Starting a new curve while another transition is in progress therefore continues from the interpolated current level rather than jumping back to the previous target.

### Perceptual intensity mapping

The former hard 12% intensity floor was removed. Large-motor strength now uses a sublinear perceptual curve:

```text
output = input ^ 0.78
```

This makes low-amplitude vibration easier to feel without converting every nonzero request into the same minimum-strength rumble. The original ordering of analog values is preserved, and the existing Phone Rumble Strength setting continues to scale the final result.

The PS2 small motor remains binary and therefore uses a fixed enabled level, matching the behavior of the original hardware signal.

### Safe delayed player release

Stopping a channel schedules its release after the fade-down completes. Each channel owns a generation counter:

1. Scheduling or starting a new transition increments the generation.
2. The delayed release captures the current generation.
3. The release runs only if the generation is still current.

This prevents an old fade-down callback from destroying a player that a newer game command has already restarted.

### Engine lifecycle and interruption recovery

The haptic engine now tracks whether it is already running, avoiding redundant `startAndReturnError` calls during an active session.

Core Haptics can stop or reset its engine when the app backgrounds, the audio session changes, or the system reclaims the engine. The stopped and reset handlers now:

- Mark the engine as stopped.
- Release both continuous players on the main queue.
- Clear the previously applied native motor state.
- Mark the originating controller slot for resynchronization.

The next CPU-side rumble pump bypasses its unchanged-value optimization for that slot. A game that continues holding the same motor value can therefore rebuild its Taptic Engine output after returning to the app instead of remaining silent until the game changes the value.

The slot-specific resynchronization mask prevents one controller slot from consuming another slot's recovery request.

### Resource management

- The continuous players are reused while their channels are active.
- The engine is started once per active haptic session rather than once per motor update.
- Players are released after their fade-down finishes.
- `autoShutdownEnabled` remains enabled, allowing Core Haptics to release idle engine resources.
- No display link, repeating timer, polling loop, or per-frame pattern generator was added.
- Transient players are created only for debounced rising edges.
- All retained continuous players and engine state are cleared by the existing native-rumble teardown path.

## Performance impact

The change is designed to keep emulation overhead negligible:

- Envelope work runs only when the packed rumble command changes or when an interrupted channel must be restored.
- Each update performs a small number of scalar calculations and schedules at most two parameter curves.
- The CPU emulation, GS renderer, audio, controller polling frequency, and SDL controller-rumble path are unchanged.
- Persistent advanced players avoid repeatedly constructing continuous patterns during active rumble.
- The 25 ms onset debounce bounds transient creation during rapidly toggling effects.
- Automatic idle shutdown prevents the phone haptic engine from remaining active after vibration finishes.

This is a haptic-quality and reliability improvement; it is not expected to change emulation FPS.

## Compatibility and fallback behavior

- The improved synthesis is used only when the device reports Core Haptics support and phone rumble is the applicable fallback.
- Physical controller SDL rumble behavior is unchanged.
- Native controller-locality haptic handling is unchanged.
- Joy-Con safeguards are unchanged.
- Devices without a Taptic Engine continue using the existing Swift/UIFeedbackGenerator fallback.
- The Phone Rumble Strength preference continues to control the final intensity.

Because PlayStation 2 software exposes only a large analog motor and a small binary motor, the app cannot recover DualSense HD waveform data that the original game never supplied. The new path synthesizes a more expressive DualSense-like feel from the available two-motor signal rather than claiming exact DualSense reproduction.

## Changed file

- `platforms/ios/app/src/main/cpp/IOS/GamepadHaptics.mm`

## Validation

- Full unsigned iOS IPA build completed successfully with `build-ios-ipa.sh`.
- Objective-C++ compilation and Core Haptics API linking completed successfully.
- `git diff --check` passed for the changed haptics file.

Generated build:

```text
platforms/ios/build-ios-xcode/ARMSX2-iOS-unsigned.ipa
```

## References

- [Core Haptics](https://developer.apple.com/documentation/corehaptics)
- [Preparing your app to play haptics](https://developer.apple.com/documentation/corehaptics/preparing-your-app-to-play-haptics)
- [Updating continuous and transient haptic parameters in real time](https://developer.apple.com/documentation/corehaptics/updating-continuous-and-transient-haptic-parameters-in-real-time)
- [CHHapticParameterCurve](https://developer.apple.com/documentation/corehaptics/chhapticparametercurve)
- [CHHapticAdvancedPatternPlayer](https://developer.apple.com/documentation/corehaptics/chhapticadvancedpatternplayer)
2026-07-31 16:49:50 +02:00
J1coding 00ee8185b8 iOS: read JIT activity under the validation lock
Follow-up to the keepalive work in the hybrid JIT safety change. The lock it
adds closes the race between the idle canary and code memory being unmapped,
which is the important half, but the activity check sits outside that lock and
leaves a smaller gap behind.

WaitForJITValidation drains by taking the mutex and dropping it again, so it
only ever waits for a handler that has already acquired the lock. A handler that
passed the activity check but has not reached the acquire yet is invisible to
it. The boot path sets the VM active, cancels the timer and drains, all of which
that handler misses, and then it carries on into BeginCodeWrite and flips
protection across the whole arena while the EE thread is executing out of it.
That is the same shape as the Devil May Cry crash, and the comment above
ARMSX2JITWorkerBusy already describes the consequence as an instant instruction
abort.

Reading activity inside the lock leaves only two possible orderings and both are
fine. Either the handler gets there first and the drain waits for it to restore
the canary byte, or it gets there second, sees the VM is busy and returns
without touching anything.

The window is a few instructions against a twelve second timer, so nobody was
going to hit this on purpose, but it costs two lines to remove.
2026-07-31 16:06:07 +02:00
Brandon 45368481be iOS Hybrid JIT Safety, No-JIT Fallback, and Interpreter Performance
# iOS Hybrid JIT Safety, No-JIT Fallback, and Interpreter Performance

## Summary

This pull request preserves the current master JIT path while making iOS game boot reliable when JIT is unavailable, revoked, or unable to allocate executable code memory. Instead of continuing into recompiler-only code with missing mappings, ARMSX2 selects the existing interpreter providers and disables only the facilities which require generated native code.

It also adds a conservative predecoded EE interpreter block cache and enables link-time optimization for the iOS PCSX2 core. Together, these changes improve the practical No-JIT path without changing the normal JIT execution architecture.

Base revision after fetching the latest master:

`1a5fc1c3731dd98f84eb62c7d9ac948241743ce7`

The working branch and `origin/master` were already identical at that revision, so no commit replay or conflict resolution was required.

## Problems addressed

### Booting without JIT could crash or remain on a black screen

The emulator previously continued through code paths which assumed executable code memory existed. If iOS had not granted JIT, had revoked it, or the executable mapping could not be allocated, null or unavailable code-cache memory could still reach:

- The EE and IOP recompilers.
- microVU0 and microVU1.
- Generated VIF unpackers.
- The software GS scanline JIT.
- Fastmem setup.
- MTVU paths which depend on generated VIF execution.

The new capability-driven fallback allows the VM to start using existing interpreter implementations instead of invoking those incompatible providers.

## Performance impact

### No-JIT EE execution

The user-observed EE time changed from **37.55 ms to 30.55 ms** after the conservative cache and ThinLTO baseline was introduced.

That is:

- **7.00 ms less EE time** in the observed workload.
- Approximately **18.6% lower EE processing time** for that workload.
- Approximately **1.23x the previous EE throughput**, if all other conditions are equal.

This is an observed device result supplied with the change, not a benchmark performed during this packaging step. Results remain game-, scene-, device-, thermal-, and settings-dependent.

The fixed block cache uses approximately 1 MiB of static memory on 64-bit builds. It trades that bounded allocation and per-block RAM validation for fewer repeated instruction decodes.

### Normal JIT gameplay

The EE block cache is not called from the JIT's `recExecute()` path. No cache lookup, validation, or interpreter callback is added per JIT-generated instruction.

The validation mutex is used only during idle JIT checking, startup transitions, backend switching, and teardown. The keep-alive timer is stopped before gameplay.

ThinLTO may provide a small native-core improvement, but no specific steady-state JIT gain is claimed.

### JIT validation could race code-memory teardown

Master's JIT keep-alive validates more than the `CS_DEBUGGED` process flag: it checks the active writable code mapping by writing a temporary canary byte, reading it back, and restoring the original byte.

That stronger validation is retained. The problem addressed here is synchronization: canceling a dispatch source prevents future callbacks but does not wait for a callback which is already executing. Without a shared lifetime boundary, a callback could retain the mapping address while another path dismantled the code cache.

### Persistent workers cannot safely change backend in place

A JIT worker owns executable mappings and initialized recompiler providers. An interpreter worker deliberately owns neither. Reusing one worker as the other backend can leave incompatible memory and provider state.

Backend changes now perform a complete worker teardown and recreation rather than attempting to mutate an initialized worker.

### The instruction-by-instruction EE interpreter repeated decode work

No-JIT EE execution previously fetched and decoded every instruction every time it executed. Repeated loops therefore paid the same opcode lookup cost continuously.

A bounded block cache now stores short, validated sequences of instruction words and their predecoded opcode descriptors.

## JIT behavior preserved from master

The normal JIT path remains the preferred path whenever executable code memory is available.

- Fresh-launch worker preparation remains intact.
- The persistent CPU worker and condition-variable wait model remain intact.
- The configured iOS JIT script protocol is still applied by the existing gate.
- `CS_DEBUGGED` validation remains intact.
- Master's writable code-memory canary remains intact.
- The 12-second idle validation interval is unchanged.
- The canary is skipped while the VM or CPU initialization is active.
- JIT-enabled sessions retain EE, IOP, VU0, VU1, VIF, software-GS, fastmem, and MTVU acceleration as configured.
- Temporary No-JIT fallback does not overwrite the user's saved recompiler preferences.
- No interpreter cache lookup is performed by `recExecute()`.

## Hybrid JIT validation synchronization

`DarwinMisc` now owns a private mutex covering the validation canary and executable mapping lifetime.

The synchronization sequence is:

1. Mapping address, size, and alias offset are published while holding the validation mutex.
2. `ValidateJITAlive()` first checks whether CPU work is active.
3. An idle validation takes the mutex before reading or touching the mapping.
4. The original byte and page protection are restored before releasing the mutex.
5. Mapping teardown takes the same mutex, clears the published mapping state, and only then unmaps the aliases.

`WaitForJITValidation()` gives the iOS worker an explicit drain point:

- Before gameplay, the worker marks itself active, stops future periodic validation, and waits for any callback which already passed the idle check.
- Before backend teardown, it stops validation and waits for the same lifetime boundary before `CPUThreadShutdown()` releases executable memory.

The keep-alive dispatch source has separate ownership synchronization so concurrent start/stop operations do not create multiple timers or race timer release.

Interpreter-only sessions never start the keep-alive timer because they have no executable mapping and no JIT grant to preserve.

## No-JIT boot flow

When JIT is unavailable, the boot-scoped runtime flow is:

1. The iOS JIT gate requests interpreter mode for the new CPU worker.
2. VM data memory is allocated normally.
3. Executable code memory is omitted.
4. Recompiler providers are not reserved or initialized.
5. Runtime configuration is clamped to the providers which actually exist.
6. EE and IOP select their interpreter implementations.
7. VU0 and VU1 select their interpreter implementations.
8. VIF uses precompiled unpack functions.
9. Software GS uses its C setup, scanline, and edge functions.
10. Fastmem and MTVU are disabled for that boot.
11. The JIT keep-alive timer is not created.

If the initial JIT gate succeeds but executable allocation subsequently fails, allocation now falls back to the same interpreter path instead of aborting the boot.

These are runtime capability overrides. Saved JIT, fastmem, VU, and MTVU preferences are not rewritten, so a later worker created with valid JIT access can use the configured accelerated path again.

## Safe provider and memory handling

`SysMemory` exposes two explicit capabilities:

- `IsAllocated()` distinguishes an initialized VM memory map from early settings loading.
- `HasCodeMemory()` identifies whether native code generators can be used.

This distinction prevents startup settings loading from being mistaken for a No-JIT VM while allowing every code-generation path to gate itself after allocation.

Provider initialization is tracked explicitly. Shutdown and cache-reset paths therefore avoid touching recompilers which were never constructed.

Releasing the memory map also clears the recorded JIT address range, preventing later diagnostics or validation from treating released memory as live code.

## VIF and MTVU fallback

Generated VIF unpackers share the executable VM allocation. `CanUseVifDynarec()` now describes the actual runtime capability: generated VIF support must be compiled in and executable code memory must exist.

The capability check covers:

- Standard VIF unpack dispatch.
- MTVU unpack dispatch.
- VIF reset.
- Mode-zero unpack tables.

When generated mode-zero entries do not exist, VIF uses the existing precompiled C function table instead of dereferencing an uninitialized generated-function pointer.

MTVU is disabled in interpreter-only mode because its VIF path depends on generated unpack execution.

## Software GS fallback

The software renderer no longer resets, queries, or emits into its native scanline cache when code memory is absent.

Interpreter-only execution selects:

- `CSetupPrim`
- `CDrawScanline`
- `CDrawEdge` when antialiasing requires it

The normal generated software renderer remains unchanged when JIT memory exists, and the Metal hardware renderer is not replaced by this fallback.

## Fastmem behavior

No-JIT execution does not emit fastmem accesses, so interpreter-only sessions skip the 4 GB virtual-address reservation and force fastmem off for that boot.

Settings reloads cannot silently re-enable fastmem against a missing reservation. The existing iOS behavior for a genuine fastmem allocation failure also remains: the VM continues without fastmem instead of terminating startup.

## Persistent-worker backend switching

The CPU worker records whether it was initialized with JIT capability.

When a later boot requests a different backend:

1. The current worker receives an exit request.
2. Idle validation is stopped and drained.
3. `CPUThreadShutdown()` releases the matching providers and mappings.
4. The exiting worker clears its creation/backend state and notifies waiters.
5. The caller waits for actual teardown rather than relying on a fixed sleep.
6. A new worker is created for the requested backend.

This prevents overlapping workers, duplicate memory reservations, and reuse of stale JIT mappings.

## Conservative EE interpreter block cache

The new `no-jit-improvements` component accelerates only the EE interpreter.

### Cache structure

- 4,096 direct-mapped cache slots.
- Up to 16 EE instructions per slot.
- Fixed process-lifetime allocation; no heap allocation occurs in the execution loop.
- Each entry stores the original instruction words and pointers to their decoded opcode descriptors.
- Cache entries are aligned to reduce false sharing and keep slot access predictable.

### Execution

On a cache hit, the interpreter reuses the decoded opcode descriptors and executes the short sequence through the existing interpreter functions. It stops immediately if:

- The program counter no longer matches the expected instruction.
- An exception or other control transfer changes the PC.
- A branch boundary is reached.

If the address cannot be cached safely, the original one-instruction `execI()` path is used.

### Correctness safeguards

- Instruction bytes are compared with current emulated RAM before every reuse.
- Blocks terminate after branches, stores, and COP0 instructions which can change memory or address-translation state.
- EE cache-clear notifications eagerly invalidate overlapping entries.
- A complete reset occurs at interpreter reset and shutdown.
- Large or wrapping invalidation ranges trigger a complete cache reset.
- Debug/development oracle configurations retain the original instruction path.
- The cache is disabled when EE cache emulation requires an instruction view which differs from RAM.

This is intentionally conservative. It does not add direct-threaded dispatch, superinstructions, or new MMI/VU NEON implementations.

## ThinLTO for the iOS core

The iOS Xcode generation script now enables `LTO_PCSX2_CORE`.

PCSX2's existing CMake support applies interprocedural optimization to the selected core source set. This can reduce native call and optimization boundaries in both JIT-enabled and interpreter builds.

ThinLTO does not alter the runtime-generated EE JIT blocks and is not expected to transform JIT performance. Its largest relevance here is reducing host-side overhead around the interpreter and core helpers.

## iOS build stability

The generated Xcode target uses per-file optimized Swift compilation with batch mode disabled. This limits peak compiler memory use for the large SwiftUI source set while preserving optimized Release emission.

This is a build-time setting and adds no runtime work.

### Idle menu

The existing 12-second validation cadence is unchanged. Each idle check validates one mapping byte, restores it, and exits. Interpreter-only workers create no validation timer.

### No-JIT resource use

Interpreter fallback avoids resources which cannot improve No-JIT execution:

- No EE/IOP/VU native code caches.
- No generated VIF cache.
- No software-GS JIT cache.
- No 4 GB fastmem virtual reservation.
- No MTVU worker dependent on generated VIF execution.
- No JIT keep-alive source.

No-JIT remains materially slower than a valid ARM64 recompiler. The purpose is safe boot plus a measurable reduction in interpreter overhead, not parity with JIT.

## Behavior matrix

| Runtime state | EE/IOP | VU0/VU1 | Code cache | Fastmem | MTVU | VIF | SW GS | Keep-alive |
|---|---|---|---:|---:|---:|---|---|---|
| Valid JIT grant | Recompiler as configured | microVU as configured | Allocated | Configured/available | Configured | Generated | Generated | Idle only |
| No `CS_DEBUGGED` grant | Interpreter | Interpreter | None | Off | Off | Precompiled | C functions | Off |
| Executable allocation failure | Interpreter | Interpreter | None | Off | Off | Precompiled | C functions | Off |
| Active JIT gameplay | Recompiler as configured | microVU as configured | Allocated | Configured/available | Configured | Generated | Generated | Stopped |

## Files changed

### JIT lifecycle and iOS worker

- `common/Darwin/DarwinMisc.cpp`
  - Synchronizes validation with executable mapping publication and release.
  - Preserves and safely drains the writable code-memory canary.
- `common/Darwin/DarwinMisc.h`
  - Documents the interpreter capability override and exposes the validation drain API.
- `platforms/ios/app/src/main/cpp/IOS/SceneDelegate.mm`
  - Adds backend-aware persistent-worker recreation and keep-alive ownership synchronization.
- `platforms/ios/app/src/main/cpp/ios_main.mm`
  - Prevents settings repair from re-enabling recompilers during an interpreter-only boot.

### Runtime capability and No-JIT fallbacks

- `pcsx2/Memory.cpp`
- `pcsx2/Memory.h`
- `pcsx2/VMManager.cpp`
- `pcsx2/Vif_Dynarec.h`
- `pcsx2/Vif_Unpack.cpp`
- `pcsx2/MTVU.cpp`
- `pcsx2/GS/Renderers/SW/GSDrawScanline.cpp`
- `pcsx2/vtlb.cpp`
- `pcsx2/vtlb.h`

These files make executable code memory an explicit capability and gate EE, IOP, VU, VIF, software-GS, fastmem, and MTVU behavior accordingly.

### EE interpreter performance

- `pcsx2/no-jit-improvements.cpp`
- `pcsx2/no-jit-improvements.h`
- `pcsx2/Interpreter.cpp`
- `pcsx2/CMakeLists.txt`

These files implement and register the validated predecoded EE block cache.

### iOS build configuration

- `platforms/ios/scripts/generate-ios-xcode.sh`
  - Enables the existing PCSX2 core LTO target.
- `platforms/ios/app/src/main/cpp/CMakeLists.txt`
  - Uses per-file optimized Swift compilation to reduce build-time frontend memory pressure.


## Explicitly not included

The subsequently evaluated POC's are absent from this PR:

- Direct-threaded EE dispatch.
- EE superinstructions.
- New ARM64 NEON MMI implementations.
- New ARM64 NEON VU implementations.

`pcsx2/MMI.cpp` and `pcsx2/VUops.cpp` remain identical to the current master revision.

## Validation

- Fetched and compared against the latest `origin/master`.
- Confirmed the local branch and remote master resolve to the same base revision.
- Full unsigned iOS IPA build completed with `platforms/ios/scripts/build-ios-ipa.sh` before packaging.
- `git diff --check` completed without whitespace errors.
2026-07-31 15:56:20 +02:00
J1coding cd40a1f169 iOS: stop building against an M1 baseline
Crash report from an iPhone XS on 2.5.0: SIGILL less than a second after launch,
in ElfObject::GetCRC during the game list scan. The exception code carries the
offending instruction, 0xce000c40, and that decodes as eor3, an ARMv8.2 SHA3
instruction. An A12 has no SHA3.

The iOS BuildParameters has been handing clang -mcpu=apple-m1, under a comment
claiming iOS devices and Apple Silicon Macs share a minimum spec. They do not.
The oldest phone that can install at our deployment target is an iPhone XS, and
an M1 baseline lets clang fold the XOR chains in the hash and CRC loops into
eor3 and bcax.

It is not one unlucky function either. The shipped 2.5.0 binary has 36 eor3 and
16 bcax spread across twelve of them, including LoadBIOS, mVUcomputeProgramHash,
the memory card CRC and save paths, and the XXH3 hashing the texture cache leans
on. An A12 reaches one of those almost immediately whatever it does, so the app
has never really worked on that generation: iPhone XS, XS Max and XR, iPad Air 3,
iPad mini 5, iPad 8, and the A12X and A12Z iPad Pros.

iOS now builds with -mcpu=apple-a12, the oldest device we accept. macOS and
Catalyst keep the M1 baseline, which is correct for them. Picking the oldest
supported device rather than switching off the one offending instruction means a
future compiler that fancies some other post-A12 feature gets refused at compile
time instead of turning into another crash report.

Checked with otool either side of the change: the count of eor3, bcax, xar, rax1,
sha512, sm3 and sm4 in the binary goes from 52 to zero.

Worth knowing for next time, this flag lives in the iOS copy of BuildParameters,
not the one at the repo root. The iOS CMakeLists points CMAKE_MODULE_PATH at its
own cmake directory, so the root copy has no say in an iOS build at all.
2026-07-31 15:19:28 +02:00
J1coding 503c1728fc iOS: fix leaks and a threading hazard in the gamepad haptics
Read the whole of GamepadHaptics.mm after a run of bugs kept coming out of it.
Three things worth fixing turned up.

The controller rumble path was leaking four objects per rumble event: two haptic
event parameters, the event, and the pattern, all allocated and never released.
This file is manual reference counting, and the device path a few hundred lines
up gets it right, which is probably why nobody caught it. A game with a
controller connected comes through there on every change of value, so it was a
steady drip for as long as you played.

The bigger one is that s_gamepads was in use from two threads. The pump owns it
and closes pads on disconnect from the CPU thread, but the delayed rumble stop
was a dispatch_after onto the main queue that held an SDL_Gamepad pointer for
300ms and then used it, and the Joy-Con name check read the array from main as
well. Worse, the Test Rumble button in settings runs on the main thread and was
opening gamepads straight into the same array. Unplugging a controller mid
rumble, or pressing Test Rumble during a game, could land on freed memory.

The array is CPU thread only now. The SDL stop rides a per slot deadline the
pump already visits every frame, the Joy-Con verdict is worked out once when the
pad is opened and cached in an atomic, and Test Rumble hands its work to the
pump rather than doing it inline. With no VM running there is no pump to hand it
to and nothing to race, so it still goes straight through, with a fallback in
case a paused VM has stopped pumping.

Last, two fallbacks in the controller lookup were answering for slots that have
no controller of their own. One handed back the only connected pad for every
slot, the other handed back any pad with haptics. Between them a single Joy-Con
could make all four slots test positive and turn rumble off for everybody, and
player 2's rumble could come out in player 1's hands.
2026-07-31 15:19:28 +02:00
J1coding 1d0e6bea4e iOS: split the phone rumble across both motors
Tester on the last build said the phone rumble now sustains properly but plays
at one strength the whole time regardless of what the game asks for. Four things
were stacked up behind that.

The big one is that we took max() of the two motor values. The PS2 small motor
has no speed control at all, it is on or off, so it arrives here as a flat 1.0.
Taking the larger of the pair meant the moment a game touched the buzzer the
whole thing pinned to full and the heavy motor, the only one carrying any
variation, got thrown away.

The other three are in how the pattern was built. The live intensity parameter
multiplies the event's own intensity rather than replacing it, and we were
baking whatever the first rumble happened to be into the event, so that first
value became a ceiling for the rest of the burst. Sharpness was baked the same
way and then shifted again by its control, which is an offset rather than a
replacement, so it landed twice. And the sharpness curve had it backwards
against the hardware: the taptic engine puts out the most force around 0.73, and
we were sitting the binary buzzer right on top of that while the analog motor
played down at 80 Hz where you can barely feel it.

Each motor now gets its own looped channel, the heavy one low and dull, the
buzzer high and sharp, both built at full intensity so the live parameter has
room to work. Only intensity is sent at runtime now.

Test Rumble never reached any of this either. It only called the controller
path, which wants a real controller and quietly gives up without one, so on a
bare phone the button did nothing at all. It now steps the heavy motor up
through three levels and buzzes the small one, which is enough to check the
strength slider without loading a game.

Last thing, the tap fallback for hardware with no taptic engine was handed the
controller-clamped values and then divided by the full range, so it could never
get past 44 percent.
2026-07-31 15:19:28 +02:00
Brian Degenhardt 1a5fc1c373 GS: record how a self-reading draw was resolved in the per-draw ledger
The ledger's tex_hazard and barrier columns are the draw config after
HandleTextureHazards has already rewritten it. A draw that arrived with its
texture aliasing the render target and was resolved by copying the target reads
back as tex_hazard NONE, barrier 0 -- indistinguishable from an ordinary
textured draw, with the copy nowhere in the table. Reading that resolved state
as the original state is how a previous investigation concluded the copies came
from texture-cache invalidation when they come from hazard handling.

So record the road taken, on a new self_read column: TEX_IS_FB, BARRIER,
DEPTH_DIRECT or COPY, blank when the source did not alias the target at all.
It is set pessimistically at the top of hazard handling and corrected by each
exit that avoids the copy, because the function has too many early returns for
a single assignment to cover.

This is what the copies actually cost, and it is not visible anywhere else:
Rogue Galaxy's church scene records 67 COPY draws per frame at autoFlush 2
against 2 at autoFlush 0, and they fall in exactly two runs of consecutive
draws per frame, 11 and 56 long, each run writing one render target.
2026-07-30 21:55:58 -07:00
Brian Degenhardt 17b2be058a GS: record a complete dump under the pipelined back-thread split
The dump's transfer and ReadFIFO hooks sit on the parse path, and its initial
state came from Freeze() on the renderer. Under GSBackThreadMode=Pipelined the
parse path belongs to the front object, so both were reading the wrong object:
the front's transfers never reached the dump at all. A Rogue Galaxy capture that
should be 39.4 MB of packets came out with 90 KB -- 0.2% of the stream, the
ReadFIFO and VSync packets alone -- and replayed as nothing. GSQueueSnapshot
warned about it rather than fixing it (GV7-2).

The dump stays owned by the renderer, which opens and closes it on the present
path; the parse side reaches it through GetDumpSink(), which routes via
m_mem_target, and the initial freeze goes through a new m_parse_target, the
inverse pointer. Both paths run on the MTGS thread -- the front's runahead is
over the back thread, not over the thread handling vsync -- so the front writes
straight into the back's dump with no synchronisation. m_parse_target->Freeze()
is the same call GSfreeze makes for a savestate, which already drains and
already takes registers from the front and local memory from the back.

Verified on a Rogue Galaxy savestate, frame-stepped over PINE so both arms start
from the identical state: the mode 3 dump is byte-identical to the mode 0 dump,
4.2 MB of initial state and 39.4 MB of packets, and it replays in gsrunner to
ten frames identical under both modes. Two mode 0 runs are likewise identical,
so the harness has no slack. Reverting just the transfer sink reproduces the
90 KB dump, so the comparison has teeth.

Two bytes of bookkeeping ride along: GSQueueSnapshot loses the warning, and the
MsgGSDump reply loses pipelined_incomplete, which now has nothing to report.
Whether the split engaged is a genuine question, so it moves to the stats reply
as gs_front_parser, next to gs_back_thread_pct where it belongs.
2026-07-30 21:55:58 -07:00
Brian Degenhardt 7f93a80dd7 PerformanceMetrics: count the GS back thread
Under GSBackThreadMode >= Lockstep roughly half the GS work moves to a second
thread, and every surface that reports GS cost -- OSD, PerfLog, the Qt status
bar, PINE stats, gsrunner's @HWSTAT@ block -- measured the MTGS thread alone.
So the split read as a large GS saving. It is not: on a Rogue Galaxy savestate
here, mode 0 costs 15.8% / 2.63 ms and mode 3 costs 17.0% / 2.84 ms plus
14.2% / 2.37 ms on the back thread -- about twice the total GS CPU time, bought
to halve the critical path. That is a real trade, but nobody could see it.

The back thread registers its own handle at entry, as the SW rasterizer workers
do; StopBackThread clears it after the join. Unlike every other handle here it
is written by a thread other than the one sampling it, so the handle and its
running total sit behind a mutex taken twice a second. Installing a handle
rebases the total off it, so the first window after a GSreopen respawn measures
the new thread rather than its difference against the retired one's.

The figure is omitted, not reported as zero, wherever a back thread does not
exist -- otherwise a mode 0 vs mode 3 comparison reads a permanent 0% as
meaningful. gsrunner latches the presence flag during the run because DumpStats
executes after VMManager::Shutdown, by which point the thread has joined.
2026-07-30 21:55:58 -07:00
Brian Degenhardt 975e408ed5 PINE: add a GS-dump opcode so a script can capture without a hotkey
MsgGSDump (0x14, ARMSX2-local) queues a GS dump of the next N frames:
[u32 frames][u32 path_len][path bytes], where frames == 0 stops a recording
dump and UINT32_MAX records until stopped -- the same press/release pair the
GSDumpMultiFrame hotkey binds. The reply is JSON carrying the resolved dump
path, so a client knows the file to wait for instead of guessing at the
snapshots folder's auto-naming.

Three things the naive version of this gets wrong, all found by testing it
against a live Dragon Quest VIII:

QueueSnapshot honours a caller-supplied path only when it ends in .png, and
silently substitutes an auto-named file otherwise -- a scripted client would
write somewhere it never looks. Normalise the path up front instead, dropping
a .gs/.gs.xz/.gs.zst/.png suffix if the caller spelled one out so that naming
the file you want does not earn a doubled extension.

A request that arrives while a dump is already recording creates no second
dump: the VSync handler only opens one when none exists. It writes a stray
screenshot, and worse, overwrites the running dump's remaining frame count and
cuts it short. The first version of this replied with a path for a file that
was never created and truncated the recording that was. Refuse instead, with
reason "already recording"; the caller can stop the running dump first. The
same defect reachable via the Screenshot hotkey is left alone here -- it is a
renderer behaviour change and belongs in its own commit.

The PINE thread cannot push MTGS packets: the ring is single-producer and that
producer is the EE thread. Take the same two-hop route BuildStatsJson already
documents -- Host::RunOnCPUThread, then RunOnGSThread -- and read GSConfig's
compression method on the GS thread, since it decides the extension.

QueueSnapshot and GSQueueSnapshot now return whether they took the request;
existing callers ignore it. GSIsDumpRecording and GSHasFrontParser expose the
two pieces of GS-thread state the reply needs. pipelined_incomplete surfaces
the known GV7-2 gap rather than letting a script collect corrupt dumps.

Verified live: every promised path was written, refusals produced no files,
and all three dump shapes replay in gsrunner -- single-frame as 4 (2) frames,
a stopped multi-frame recording as 186 (91).
2026-07-30 21:55:58 -07:00
Brian Degenhardt bf65e8604b GameDB: drop autoFlush on Rogue Galaxy — a deliberate speed/accuracy trade
Rogue Galaxy is the slowest title we track on handhelds and users report it as
such. Turning autoFlush off is the largest lever we have found for it: render
passes -38%, texture copies -74%. On the Adreno 610, which has no headroom, that
is -1.82 ms/frame and +2.6 fps. On the Adreno 650 it is -1.67 ms banked as
headroom, both arms already at 100% speed.

This is not free and should not be recorded as if it were. The software
renderer, an exact per-pixel GS model and an independent oracle here because
AutoFlushSW is a separate setting, scores level 0 3.4x further from truth than
level 2 on the contested pixels (mean error 9.436 vs 2.808; level 2 is closer on
17312 of 22424). What degrades is the light a lamp contributes to nearby lit
surfaces, so chests, blades and floors read slightly bright and warm. The glow
cones themselves are pixel-identical.

It is taken because the error is imperceptible in practice: bounded at 21-23/255
in all three captured scenes, diffuse rather than a missing object, and four
independent side-by-side looks at 1:1 failed to distinguish the two. Revert to
level 1 -- not 2 -- if anyone reports a regression: level 1 is pixel- and
cost-identical to 2 on Rogue Galaxy at 1x, 3x and 6x, and since 381bc41ded it is
also worth -5.8% of GS-thread cycles because it moves the game's non-sprite prims
onto the direct vertex kick. Level 2 buys nothing measurable over level 1 here.

Seven serials, which is every Rogue Galaxy entry the overlay carries. The Korean
release is SCKA-30005 and upstream gives it no gsHWFixes at all, so it is absent
here too rather than newly missed.

⚠ SLKA-25372 is Black, not Rogue Galaxy -- it is Criterion's Burnout engine,
which is why it carries OI_BurnoutGames. An earlier working copy had it in the
Rogue Galaxy set and flipped it to 0; it stays at 2.
2026-07-30 21:55:58 -07:00
Brian Degenhardt f0aa0f1949 GS: take the direct vertex kick for non-sprite prims at autoflush SpritesOnly
The auto_flush instantiations of the vertex handlers exist to feed
HandleAutoFlush, which reads the incoming vertex out of m_v. To do that they
stage every vertex through m_v instead of keeping it in registers, which is why
SetPrimHandlers hands the same auto_flush argument to every primitive type.

At SpritesOnly that is wasted on everything that is not a sprite. IsAutoFlushDraw
early-outs on the prim before it looks at anything else, so those prims write a
staged vertex, read it once, and discard it. Narrow the template argument per
prim so they take the fused direct kick instead, mirroring IsAutoFlushDraw's
early-out exactly -- it keys on the level alone and not on the renderer, so the
software path narrows in step.

Dragon Quest VIII renders identically at levels 1 and 2 (same draws, passes and
copies), so level 2 is an exact staged control for level 1's direct path with no
rendering difference to confound it. GS-thread cycles over 3 runs each, 240
frames: 2043.2M staged against 1947.4M direct, ranges disjoint, -4.7%. The parse
handler itself goes 272.5M -> 156.2M, so it accounts for essentially the whole
delta. Rebuilding the old handler table and diffing against it agrees: -5.1%.

Output is unchanged, as it must be: prims, draws, render passes and copies are
identical on Dragon Quest VIII and Rogue Galaxy, and all four dumped frames are
pixel-identical under both the hardware and the software renderer.

581 GameDB entries ship autoFlush: 1.
2026-07-30 21:55:58 -07:00
Brian Degenhardt 80e4d09b99 GS/VK: arm the mid-frame submit kick in frames, not render passes
The kick's arming window exists to answer one question -- has this game read
back recently enough to be worth kicking for -- so that titles which never read
back see zero change. It counted render passes, and 128 passes means completely
different things in different titles: about three frames of OutRun 2006, but
only about three quarters of a Rogue Galaxy frame. So RG armed the window at its
one readback per frame, spent it partway through, and then ran the rest of every
frame with the kick silently switched off. Nothing asked for that; it fell out of
the unit.

Count the window in frames since the last readback instead, which is the unit the
comment already claimed ("~a few frames' worth of render passes") and the unit the
decision is actually about. The cadence stays in render passes, where a uniform
interval is what you want. The never-read-back guarantee is unchanged and still
carried by the ~0u sentinel.

Measured on M2/Honeykrisp, 60-90 frames per dump, gsrunner without -perf: total
GPU stall (readback wait plus command-buffer activate stall) is unmoved --
Rogue Galaxy 554ms before and 558ms after, OutRun 2006 320ms and 319ms, both
inside run-to-run spread. Shadow of the Colossus and Black, which never read
back, take zero kicks before and after. So this is not a speed change here; it
removes a scene-dependent cliff that a device where the kick matters more could
land on.

While measuring, the threshold's cost model turned out to be badly wrong, so
correct the comment. "RPs-per-frame / threshold extra submits" predicts ~14
kicks/frame for Rogue Galaxy; the real figure is 2, because the fence gate -- not
the threshold -- is what binds. With three command buffers only two submissions
can be in flight, and ~3300 of ~3400 offers to kick find the next command buffer
still executing. Sweeping the threshold 8->16 measured -2% stall on Rogue Galaxy
and +12% on OutRun 2006, so it is left alone.
2026-07-30 21:55:58 -07:00
Brian Degenhardt 639b317dbc gsrunner: stop the Wayland message pump blocking past the shutdown flag
The pump polls the display fd with a 16 ms cap so it can re-test the shutdown
flag between polls, but on POLLIN it called wl_display_dispatch(), which reads
the queued events and then waits for more. A window nobody is drawing to gets
no further events, so the flag was never re-tested and the process never exited
-- gsrunner would print its whole stats block and then hang forever, leaving
every automated run to be killed by a timeout.

Switch to the non-blocking read sequence: prepare_read, flush, poll, then
read_events or cancel_read, then dispatch_pending. Nothing in the loop can
block now, so the cap does what its comment claims.
2026-07-30 21:55:58 -07:00
Brian Degenhardt b2d7a8d0a6 GS: serve 1:1 same-format StretchRects as image copies
A StretchRect is a draw, so it needs a render pass of its own and the pass it
interrupted has to be restarted afterwards -- two pass boundaries. When the
stretch is really a plain 1:1 copy between identically-formatted textures, the
backend's image-copy path does the same work for one.

The texture cache hits this constantly. A target-backed source is destroyed
outright whenever anything writes its target, so every autoFlush split
re-copies the sampled region of the render target it has just written.

The gate is narrow enough that the two paths cannot disagree on any pixel:
plain COPY/DEPTH_COPY with a full write mask, identical formats, depth-vs-colour
aspect agreeing on both sides, a source that actually holds contents rather
than a pending clear, rects that land on the texel grid at 1:1, and both rects
in bounds -- the draw path scissors an out-of-range destination and edge-clamps
out-of-range source coordinates, and a copy can do neither.

Render passes over a 5-loop gsrunner replay, Vulkan / OpenGL:

  Rogue Galaxy   2411 -> 1741  /  2283 -> 1373
  OutRun 2006    1240 -> 1118  /   811 ->  657
  Black          1080 -> 1010  /   282 ->  202
  God of War II  1278 -> 1238

Draw counts are unchanged everywhere. Colour output is bit-identical on Vulkan
across all six staged dumps at 1x and 3x, and on OpenGL for the dumps that
render deterministically there.
2026-07-30 21:55:58 -07:00
Brandon eac3f937a4 Added proper values for Right Side of Screen
Removed conditional swipe zone center calculation and replaced it with a fixed position for consistency.
2026-07-31 01:19:58 +02:00
Brandon bcdc98ac27 iOS: Improve Dynamic Thumbstick Feedback, iPad Swipe Camera, and Virtual Layout Management
# iOS: Improve Dynamic Thumbstick Feedback, iPad Swipe Camera, and Virtual Layout Management

## Summary

This pull request improves three parts of the iOS Virtual Pad experience:

- Changes the Dynamic Thumbstick maximum-pull indicator from a continuously repeating animation into a single pulse for each threshold crossing.
- Expands Swipe Camera input across the complete left half of the screen on iPad while preserving the existing right-half behavior on iPhone.
- Replaces the direct Share button beside saved layouts with a three-dot menu that provides Rename, Share, and Delete actions.

The changes are limited to the iOS SwiftUI Virtual Pad controls and settings. Emulator-core input, controller mapping, saved layout formats, and iPhone Swipe Camera placement are unchanged.

## Base

- Repository: `ARMSX2/ARMSX2`
- Target branch: `master`
- Rebased master commit: `12a7e3768`

## Changes

### One-shot maximum-pull pulse

Previously, reaching maximum pull set a Boolean state that drove a repeating SwiftUI animation. The origin continued pulsing for as long as the touch remained beyond the maximum-pull threshold.

The new implementation treats reaching maximum pull as an event:

1. The existing overextension threshold and hysteresis determine when the gesture crosses into maximum pull.
2. A pulse token increments only on the transition from not overextended to overextended.
3. The visual grows with a short spring animation.
4. It returns to its resting scale and opacity with an ease-out animation.
5. It remains at rest while the touch stays overextended.
6. Moving back below the hysteresis boundary allows a later threshold crossing to trigger one new pulse.

This behavior applies to:

- Normal Dynamic Thumbsticks
- Swipe Camera after it converts into Dynamic Joystick mode

The existing maximum-pull haptic remains tied to the same threshold-crossing event.

### Full left-side Swipe Camera input on iPad

On iPad, Swipe Camera was positioned over the upper/right-side input region and did not cover the intended touch area.

The Swipe Camera view now retains its half-screen width but uses a device-specific horizontal position:

- iPad: the full left half of the gameplay surface
- iPhone: the existing right half of the gameplay surface

Only the input-zone placement changes. Swipe sensitivity, Dynamic Joystick conversion, camera output, touch lifecycle, and visual settings continue using the existing implementation.

### Saved-layout three-dot menu

Each saved layout previously displayed a dedicated Share icon. It is now replaced by an accessible three-dot menu with:

- **Rename Layout**
  - Opens a text-field alert initialized with the current display name.
  - Uses the existing preset-store rename operation.
  - Reports rename errors through the existing layout message alert.

- **Share Layout**
  - Uses the existing layout export and system share-sheet flow.
  - Does not change the exported layout format.

- **Delete Layout**
  - Uses a destructive menu role.
  - Requires confirmation before deletion.
  - Uses the existing preset-store deletion operation so active global or per-game references fall back according to the store’s established behavior.
  - Reports deletion errors through the existing layout message alert.

The menu has a descriptive accessibility label containing the layout name.

## Files Changed

- `platforms/ios/app/src/main/swift/Views/Controller/DynamicThumbstickControls.swift`
- `platforms/ios/app/src/main/swift/Views/VirtualControllerView.swift`
- `platforms/ios/app/src/main/swift/Views/Settings/VirtualPadSettingsView.swift`

## User-visible Behavior

### Before

- Maximum-pull origin feedback repeated indefinitely while held past maximum pull.
- iPad Swipe Camera accepted input only from the previous limited/right-side zone.
- Saved layouts exposed only a direct Share button.

### After

- Maximum pull produces one clear pulse per threshold crossing.
- iPad Swipe Camera accepts gestures throughout the full left half of the gameplay surface.
- Saved layouts expose Rename, Share, and Delete from one compact three-dot menu.
- iPhone Swipe Camera placement remains unchanged.

## Compatibility and Data Impact

- No changes to the layout file schema.
- No migration is required.
- Existing saved layouts remain compatible.
- Existing layout export/import behavior remains compatible.
- No changes to emulator-core controller input.
- No changes to physical-controller support.
- No changes to Dynamic Thumbstick sensitivity or deadzone calculations.
- The implementation remains within the existing iOS 17+ deployment target.

## Validation

- Rebased onto the latest `origin/master` without conflicts.
- `git diff --check` passes.
- Full unsigned iOS Release IPA build completed with `platforms/ios/scripts/build-ios-ipa.sh`.
2026-07-31 01:19:58 +02:00
jpolo1224 12a7e37682 Android: 2D fallback background, portrait status layout, Clear Shader Cache placement, memcard delete wording 2.6.6.2 2026-07-30 18:20:12 -04:00
jpolo1224 1a45319a6a Android: fix Auto Progressive Scan hold, and correct Samsung QHD touch offset 2026-07-30 18:20:12 -04:00
jpolo1224 9d735f9ccb GameDB: no readbacks for Need for Speed Underground 1 & 2 2026-07-30 18:20:12 -04:00
jpolo1224 036eeea43e Patch: apply cheats filed under the generic all-CRC name 2026-07-30 18:20:12 -04:00
jpolo1224 e0f39849ce GS: keep Adreno framebuffer-fetch on by default, gate Snapdragon 8 Elite off 2026-07-30 18:20:12 -04:00
J1coding dcf56d79a9 iOS: give the phone's own rumble some range
Phone rumble came out at the same weak strength no matter what the game asked
for. Three separate things flattened it, stacked on top of each other.

The range was crushed at both ends. Everything is capped at 0x7000, which is
44 percent of full scale, and the Swift side then floored it at 0.3. The whole
chain came out as max(0.3, min(0.4375, motor / 255)), so motor bytes 1 to 76
all produced 0.300 and 112 to 255 all produced 0.4375. Of 256 possible values,
35 changed anything.

It was a tap rather than rumble. UIImpactFeedbackGenerator knocks once and
there is no way to sustain it or change it afterwards.

And a steady rumble only fired once, because the dedup gate skips a packed
value that has not changed. A game holding the motor for two seconds got one
blip.

There was already a continuous CoreHaptics implementation sitting in this file,
written and never called by anything: a looped continuous event with an advanced
player whose intensity is updated live. It was controller specific in two lines,
so it now creates a device engine instead and the phone gets sustained rumble
that tracks the motor. That deleted the dead path rather than adding a new one.

The phone reads the packed value unclamped, so it gets the whole range. The
0x7000 cap stays where it was tuned, on the controller motors. Multiplied by a
new Phone Rumble Strength slider under Virtual Pad, Feedback, at full by
default. Devices with no taptic engine keep the old tap, minus the 0.3 floor.

Reviving the dead code meant fixing what it had been getting away with while
nothing ran it. The engine comes from alloc/init now so the static owns it, the
player still comes from a factory method and needs the retain, and the dynamic
parameter array was leaking on every single intensity update. The stopped and
reset handlers hop to main before touching the player, since CoreHaptics calls
them back on its own queue and everything else here runs on main.

The zero has to reach the engine too. A looped player runs until told otherwise,
so wiring it up only where rumble starts would leave the phone buzzing after the
game stopped asking.
2026-07-31 00:18:48 +02:00
J1coding a255bbe9fd iOS: reconnect rumble to the core
Controller rumble and phone rumble have both been dead since the move to a
single shared core on the 8th of July, so 2.4.1, 2.5.0 and 2.5.1 all shipped
without either.

ARMSX2_iOSUpdatePadVibration is the only thing that ever writes the iOS rumble
queue, and nothing has called it since that move. It used to be hooked into
InputManager::SetPadVibrationIntensity as a patch on the iOS tree's own copy of
the core, and when we adopted the shared one the patch did not come along. One
dead producer starves all three consumers, which is why SDL rumble, the
CoreHaptics pulse and the phone's taptic fallback went silent together rather
than one at a time.

Android hit exactly this from exactly this migration and was fixed three days
later. That fix is still sitting in the same function saying so in its comment.
The iOS block goes right beside it so the two read as a pair.

Note InputManager.cpp had no TargetConditionals.h, so TARGET_OS_IPHONE was
undefined and the guard would have compiled the whole thing back out while the
build stayed green. The include is guarded the same way Host.cpp does it.

Three more things sat behind the dead call site:

The per frame pump skipped past the phone fallback before reaching it. It ran
the rumble step only after confirming a gamepad was in the slot, and the taptic
fallback exists for the case where there is no gamepad in the slot. Hoisted
above the check, which is what makes phone rumble work rather than just
controller rumble.

Emulation Only Mode set a flag that turned phone haptics off for the rest of the
session. It is only ever cleared on a branch that returning to a stripped VM does
not take. Dropped it: the same call also releases the cached generators, trigger
builds them again on demand, so the release was already self healing and the flag
was only blocking it.

The per slot pulse engine was an autoreleased object living in a static in a file
built without ARC, then messaged again a third of a second later from a delayed
stop. Retained now, and released at both places the slot is cleared. The @try
around it never helped, since messaging freed memory does not raise.

The three other unretained statics in that file have the same defect but their
assigning function has no callers, so nothing can reach them.
2026-07-31 00:18:48 +02:00