mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
Merge remote-tracking branch 'origin/master' into jit-android-catchup-gv7
This commit is contained in:
@@ -38,6 +38,16 @@
|
||||
#include <immintrin.h>
|
||||
|
||||
#elif defined(ARCH_ARM64)
|
||||
|
||||
// AArch64 has no aligned/unaligned load distinction to begin with: LDR Q and LD1
|
||||
// take any address, and GSVector4i::load<aligned> ignores its own parameter and
|
||||
// emits the same instruction either way. Leaving this undefined made the callers
|
||||
// pay for a distinction that does not exist — a runtime address-and-pitch test
|
||||
// per texture upload, dispatching into three template instantiations that
|
||||
// compile to identical code, and for 32/16-bit columns a worse load strategy
|
||||
// (eight combining 64-bit loads instead of four 128-bit loads and a swizzle).
|
||||
#define FAST_UNALIGNED 1
|
||||
|
||||
#include <arm_neon.h>
|
||||
#endif
|
||||
|
||||
|
||||
+13
-4
@@ -482,13 +482,22 @@ namespace R5900
|
||||
// an MMIO handler page, or a physical address that does not exist --
|
||||
// is marked unbacked; the line still caches and reports its flags,
|
||||
// and loses its data on eviction (see the comment on CacheTag).
|
||||
//
|
||||
// The lookup goes through the physical map, not through the KSEG0
|
||||
// alias of the page: KSEG0 is only 512 MB wide, so routing a
|
||||
// physical page through it meant masking the tag to 29 bits, and
|
||||
// every page at or above 0x20000000 then folded into the low half
|
||||
// of the map and resolved to whatever lives there. A page past the
|
||||
// end of the map folded onto ordinary RAM and the write-back went
|
||||
// into it. vtlb_GetPhyPtr covers the whole 1 GB physical map and
|
||||
// answers null both for a handler page and for an address off the
|
||||
// end of it.
|
||||
const u32 pageTag = cpuRegs.CP0.n.TagLo & ~static_cast<u32>(CacheTag::ALL_BITS);
|
||||
const u32 alias = 0x80000000u | (pageTag & 0x1FFFFFFFu);
|
||||
const VTLBVirtual vmv = vtlbdata.vmap[alias >> VTLB_PAGE_BITS];
|
||||
const bool backed = !vmv.isHandler(alias);
|
||||
void* const host = vtlb_GetPhyPtr(pageTag);
|
||||
const bool backed = host != nullptr;
|
||||
|
||||
line.tag.setValidPFN(backed);
|
||||
line.tag.setAddr(backed ? vmv.assumePtr(alias) : static_cast<uptr>(pageTag));
|
||||
line.tag.setAddr(backed ? reinterpret_cast<uptr>(host) : static_cast<uptr>(pageTag));
|
||||
line.tag.rawValue &= ~CacheTag::ALL_FLAGS;
|
||||
line.tag.rawValue |= (cpuRegs.CP0.n.TagLo & CacheTag::ALL_FLAGS);
|
||||
|
||||
|
||||
@@ -144,6 +144,11 @@ alignas(16) extern tIPU_BP g_BP;
|
||||
MULTI_ISA_DEF(
|
||||
extern void ipu_dither(const macroblock_rgb32& rgb32, macroblock_rgb16& rgb16, int dte);
|
||||
|
||||
// The scalar oracle ipu_dither()'s vector paths are written against. Exposed
|
||||
// so the tests can hold whichever path this host selected to it; the emulator
|
||||
// only ever reaches it through ipu_dither()'s own fallback arm.
|
||||
extern void ipu_dither_reference(const macroblock_rgb32& rgb32, macroblock_rgb16& rgb16, int dte);
|
||||
|
||||
void IPUWorker();
|
||||
)
|
||||
|
||||
|
||||
+73
-3
@@ -10,22 +10,29 @@
|
||||
|
||||
MULTI_ISA_UNSHARED_START
|
||||
|
||||
void ipu_dither_reference(const macroblock_rgb32 &rgb32, macroblock_rgb16 &rgb16, int dte);
|
||||
|
||||
#if defined(_M_X86)
|
||||
void ipu_dither_sse2(const macroblock_rgb32 &rgb32, macroblock_rgb16 &rgb16, int dte);
|
||||
#endif
|
||||
|
||||
#if defined(ARCH_ARM64)
|
||||
void ipu_dither_neon(const macroblock_rgb32 &rgb32, macroblock_rgb16 &rgb16, int dte);
|
||||
#endif
|
||||
|
||||
__ri void ipu_dither(const macroblock_rgb32 &rgb32, macroblock_rgb16 &rgb16, int dte)
|
||||
{
|
||||
#if defined(_M_X86)
|
||||
ipu_dither_sse2(rgb32, rgb16, dte);
|
||||
#elif defined(ARCH_ARM64)
|
||||
ipu_dither_neon(rgb32, rgb16, dte);
|
||||
#else
|
||||
ipu_dither_reference(rgb32, rgb16, dte);
|
||||
#endif
|
||||
}
|
||||
|
||||
__ri void ipu_dither_reference(const macroblock_rgb32 &rgb32, macroblock_rgb16 &rgb16, int dte)
|
||||
// Deliberately not inlineable: this is the semantic oracle the vector paths are
|
||||
// written against, so the tests need a symbol to call. (__ri collapses to
|
||||
// __forceinline in Release, which would leave nothing to link to.)
|
||||
void ipu_dither_reference(const macroblock_rgb32 &rgb32, macroblock_rgb16 &rgb16, int dte)
|
||||
{
|
||||
if (dte) {
|
||||
// I'm guessing values are rounded down when clamping.
|
||||
@@ -121,4 +128,67 @@ __ri void ipu_dither_sse2(const macroblock_rgb32 &rgb32, macroblock_rgb16 &rgb16
|
||||
|
||||
#endif
|
||||
|
||||
#if defined(ARCH_ARM64)
|
||||
|
||||
// dither_coefficient[] above with the sign folded into the choice of operation:
|
||||
// a positive cell goes in the add table, a negative one goes in the sub table at
|
||||
// its magnitude, and the other table holds zero for that lane. Saturating byte
|
||||
// arithmetic then gives the reference's clamp to [0, 255] for free.
|
||||
//
|
||||
// One row of the source matrix covers four pixel columns and the pattern repeats
|
||||
// every four, so each entry is that row's four cells laid out four times — lane
|
||||
// k is the cell for pixel k, which is what a deinterleaved row wants.
|
||||
alignas(16) static const u8 dither_add_matrix[4][16] = {
|
||||
{0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1},
|
||||
{2, 0, 3, 0, 2, 0, 3, 0, 2, 0, 3, 0, 2, 0, 3, 0},
|
||||
{0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0},
|
||||
{3, 0, 2, 0, 3, 0, 2, 0, 3, 0, 2, 0, 3, 0, 2, 0},
|
||||
};
|
||||
alignas(16) static const u8 dither_sub_matrix[4][16] = {
|
||||
{4, 0, 3, 0, 4, 0, 3, 0, 4, 0, 3, 0, 4, 0, 3, 0},
|
||||
{0, 2, 0, 1, 0, 2, 0, 1, 0, 2, 0, 1, 0, 2, 0, 1},
|
||||
{3, 0, 4, 0, 3, 0, 4, 0, 3, 0, 4, 0, 3, 0, 4, 0},
|
||||
{0, 1, 0, 2, 0, 1, 0, 2, 0, 1, 0, 2, 0, 1, 0, 2},
|
||||
};
|
||||
|
||||
__ri void ipu_dither_neon(const macroblock_rgb32 &rgb32, macroblock_rgb16 &rgb16, int dte)
|
||||
{
|
||||
const uint8x16_t alpha_test = vdupq_n_u8(0x40);
|
||||
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
// NEON deinterleaves on the load, so a whole 16-pixel row arrives already
|
||||
// split one register per channel. The SSE path needs six unpacks to reach
|
||||
// the same place because x86 has no equivalent load.
|
||||
uint8x16x4_t px = vld4q_u8(&rgb32.c[i][0].r);
|
||||
|
||||
if (dte) {
|
||||
const uint8x16_t add = vld1q_u8(dither_add_matrix[i & 3]);
|
||||
const uint8x16_t sub = vld1q_u8(dither_sub_matrix[i & 3]);
|
||||
|
||||
px.val[0] = vqsubq_u8(vqaddq_u8(px.val[0], add), sub);
|
||||
px.val[1] = vqsubq_u8(vqaddq_u8(px.val[1], add), sub);
|
||||
px.val[2] = vqsubq_u8(vqaddq_u8(px.val[2], add), sub);
|
||||
}
|
||||
|
||||
const uint8x16_t r = vshrq_n_u8(px.val[0], 3);
|
||||
const uint8x16_t g = vshrq_n_u8(px.val[1], 3);
|
||||
const uint8x16_t b = vshrq_n_u8(px.val[2], 3);
|
||||
const uint8x16_t a = vceqq_u8(px.val[3], alpha_test);
|
||||
|
||||
// r:5 g:5 b:5 a:1, least significant field first. The alpha compare widens
|
||||
// to 0x00FF, and 0x00FF << 15 truncates to exactly the 0x8000 top bit.
|
||||
const uint16x8_t lo = vorrq_u16(
|
||||
vorrq_u16(vmovl_u8(vget_low_u8(r)), vshlq_n_u16(vmovl_u8(vget_low_u8(g)), 5)),
|
||||
vorrq_u16(vshlq_n_u16(vmovl_u8(vget_low_u8(b)), 10), vshlq_n_u16(vmovl_u8(vget_low_u8(a)), 15)));
|
||||
const uint16x8_t hi = vorrq_u16(
|
||||
vorrq_u16(vmovl_high_u8(r), vshlq_n_u16(vmovl_high_u8(g), 5)),
|
||||
vorrq_u16(vshlq_n_u16(vmovl_high_u8(b), 10), vshlq_n_u16(vmovl_high_u8(a), 15)));
|
||||
|
||||
vst1q_u16(reinterpret_cast<u16 *>(&rgb16.c[i][0]), lo);
|
||||
vst1q_u16(reinterpret_cast<u16 *>(&rgb16.c[i][8]), hi);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
MULTI_ISA_UNSHARED_END
|
||||
|
||||
@@ -3347,6 +3347,20 @@ void VMManager::CheckForMiscConfigChanges(const Pcsx2Config& old_config)
|
||||
ShutdownDiscordPresence();
|
||||
}
|
||||
|
||||
// PINE is the same shape of thing as the Discord integration above -- an optional external
|
||||
// service whose entire lifecycle is one bool plus a port -- but it was never wired to the
|
||||
// settings path. ReloadPINE() had exactly two callers, CPUThreadInitialize() and
|
||||
// UpdateDiscDetails(), so a toggle only took effect at the next app start or game change.
|
||||
// On a handheld that reads as a broken switch: the UI says enabled, nothing is listening,
|
||||
// and there is no separate window to restart into to discover otherwise.
|
||||
//
|
||||
// Called unconditionally rather than gated on old_config, because ReloadPINE() compares the
|
||||
// request against the LIVE server -- is one initialized, and on which slot -- which is
|
||||
// strictly stronger than a config diff. It early-returns when they already agree, and it
|
||||
// recovers a server that lost an earlier bind (the port still in TIME_WAIT from a previous
|
||||
// run is the usual way that happens) instead of trusting a config value that never changed.
|
||||
ReloadPINE();
|
||||
|
||||
if (HasValidVM() && (EmuConfig.EnableThreadPinning != old_config.EnableThreadPinning ||
|
||||
(s_thread_affinities_set && EmuConfig.Speedhacks.vuThread != old_config.Speedhacks.vuThread)))
|
||||
{
|
||||
|
||||
@@ -289,6 +289,20 @@ object ConfigStore {
|
||||
fun save(scope: SettingsScope, serial: String?, updated: Settings, previous: Settings? = null) {
|
||||
if (scope == SettingsScope.Game && serial != null) {
|
||||
val global = loadGlobal()
|
||||
// Process-wide fields have to go to global even from a Game-scope save, because the
|
||||
// per-game file structurally cannot hold them. PINE is one server for the whole
|
||||
// process, so Settings.merge pins it to the global value and Settings.diff never
|
||||
// emits the key -- both deliberate. The consequence was that toggling PINE from the
|
||||
// in-game menu, which saves in Game scope, wrote it NOWHERE: the override file
|
||||
// refuses the key and global was not being written. The switch stayed on only
|
||||
// because saveSettings had already updated the in-memory Settings, so it read as
|
||||
// "enabled" until the process restarted and the store answered false again.
|
||||
//
|
||||
// Promote just those fields, by copying them onto global rather than saving
|
||||
// `updated` wholesale -- `updated` is the game's resolved settings, and writing all
|
||||
// of it to global would leak every per-game value into the global layer.
|
||||
if (updated.pineEnabled != global.pineEnabled || updated.pineSlot != global.pineSlot)
|
||||
saveGlobal(global.copy(pineEnabled = updated.pineEnabled, pineSlot = updated.pineSlot))
|
||||
val overrides = Settings.diff(global, updated)
|
||||
// Every field, so a pinned key can be given its CURRENT value even when that
|
||||
// value equals global's (the diff above necessarily omits it).
|
||||
|
||||
@@ -91,6 +91,11 @@ private const val STICK_DEAD = 0.15f
|
||||
// pads; the value is re-normalized past it (see sendTrigger) so pressure ramps smoothly
|
||||
// from 0 instead of flickering on/off at a hard threshold — the jitter those pads showed.
|
||||
private const val TRIGGER_DEAD = 0.06f
|
||||
// Travel past which a trigger counts as the L2/R2 BUTTON being held rather than a pressure
|
||||
// value — what the bind capture records, what fires a trigger-bound hotkey, and what makes a
|
||||
// held trigger a combo modifier. Well above TRIGGER_DEAD: pressure ramps from a brush, but
|
||||
// "pressed" should mean a deliberate press.
|
||||
private const val TRIGGER_DIGITAL_THRESHOLD = 0.5f
|
||||
// Threshold past which a stick remapped to D-pad / face buttons registers as a
|
||||
// digital press. Higher than STICK_DEAD so a resting/wobbling stick doesn't fire.
|
||||
private const val STICK_DIGITAL_THRESHOLD = 0.5f
|
||||
@@ -3603,17 +3608,11 @@ open class MainActivityRuntime : ComponentActivity() {
|
||||
// D-pad never registered while face-button mapping (different codes)
|
||||
// worked fine.
|
||||
dispatchDpadCombined(ev, port)
|
||||
// Analog triggers (L2/R2). Xbox / DualShock / most modern pads
|
||||
// report these as 0..1 motion-axis values, not Key.ButtonL2/R2
|
||||
// key events, so the direct key path never sees them.
|
||||
// AXIS_LTRIGGER/RTRIGGER is the modern path; some controllers
|
||||
// (older Moga, certain BT mappings) report via AXIS_BRAKE/GAS
|
||||
// instead — take the max so we handle whichever the device
|
||||
// actually emits without double-driving when both are present.
|
||||
sendTrigger(ev, MotionEvent.AXIS_LTRIGGER, MotionEvent.AXIS_BRAKE,
|
||||
KeyEvent.KEYCODE_BUTTON_L2, port)
|
||||
sendTrigger(ev, MotionEvent.AXIS_RTRIGGER, MotionEvent.AXIS_GAS,
|
||||
KeyEvent.KEYCODE_BUTTON_R2, port, axisC = rightTriggerExtraAxis(ev.deviceId))
|
||||
// Analog triggers (L2/R2). Xbox / DualShock / most modern pads report these as
|
||||
// 0..1 motion-axis values, not KEYCODE_BUTTON_L2/R2 key events, so the direct key
|
||||
// path never sees them. Which axes that means per device is triggerAxes' job.
|
||||
sendTrigger(ev, left = true, port = port)
|
||||
sendTrigger(ev, left = false, port = port)
|
||||
// Physical STICK DIRECTIONS bound to a PS2 control via the "(send)"
|
||||
// rows — e.g. R-Stick Down bound to send Square. The analog "(send)"
|
||||
// targets contribute to the merge layer like every other writer.
|
||||
@@ -3702,15 +3701,48 @@ open class MainActivityRuntime : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// Extra RT axis for pads that report the right trigger on AXIS_RZ (AYANEO Xbox mode) instead of
|
||||
// RTRIGGER/GAS. Only when RZ is a 0..1 range (a real stick-Y is -1..1), so standard pads are
|
||||
// untouched. -1 = no such axis. Cached — InputDevice.getDevice is a binder call.
|
||||
private val rightTriggerAxisCache = HashMap<Int, Int>()
|
||||
private fun rightTriggerExtraAxis(deviceId: Int): Int = rightTriggerAxisCache.getOrPut(deviceId) {
|
||||
val rz = runCatching { InputDevice.getDevice(deviceId)?.getMotionRange(MotionEvent.AXIS_RZ) }.getOrNull()
|
||||
if (rz != null && rz.min >= 0f) MotionEvent.AXIS_RZ else -1
|
||||
// Which axes carry the [left]/right trigger on this pad: LTRIGGER/RTRIGGER (modern),
|
||||
// BRAKE/GAS (older Moga, some BT mappings), or plain Z/RZ when Android has no vendor key
|
||||
// layout for the pad and passes raw HID through — AYANEO Xbox mode on the right (#394), a
|
||||
// plain Xbox controller on BOTH. Only a 0..1 range qualifies (a stick axis spans -1..1), so
|
||||
// a standard pad's right stick is never taken for a trigger; -1 = absent. The left side had
|
||||
// no such fallback, so on those pads LT was read by nothing at all. One resolver for capture
|
||||
// AND gameplay, so a bind can't capture an axis gameplay doesn't read. Cached:
|
||||
// InputDevice.getDevice is a binder call and motion events are far too frequent for it.
|
||||
private val triggerAxisCache = HashMap<Int, Triple<Int, Int, Int>>()
|
||||
private fun triggerAxes(deviceId: Int, left: Boolean): Triple<Int, Int, Int> =
|
||||
triggerAxisCache.getOrPut(deviceId * 2 + (if (left) 0 else 1)) {
|
||||
val raw = if (left) MotionEvent.AXIS_Z else MotionEvent.AXIS_RZ
|
||||
val range = runCatching { InputDevice.getDevice(deviceId)?.getMotionRange(raw) }.getOrNull()
|
||||
Triple(
|
||||
if (left) MotionEvent.AXIS_LTRIGGER else MotionEvent.AXIS_RTRIGGER,
|
||||
if (left) MotionEvent.AXIS_BRAKE else MotionEvent.AXIS_GAS,
|
||||
if (range != null && range.min >= 0f) raw else -1,
|
||||
)
|
||||
}
|
||||
|
||||
/** 0..1 travel on the [left]/right trigger — highest of the candidate axes, negatives
|
||||
* clamped (some pads idle an unused trigger axis at -1). Returns **-1 when the pad has no
|
||||
* trigger axis on that side**, which is not the same as one resting at zero: a Switch Pro
|
||||
* Controller sends L2/R2 as key events only, and reading its absent axes as 0.0 once wrote
|
||||
* "released" every motion event, cancelling a held R2 whenever the stick moved. */
|
||||
private fun triggerTravel(ev: MotionEvent, left: Boolean): Float {
|
||||
val (a, b, c) = triggerAxes(ev.deviceId, left)
|
||||
if (!deviceHasAxis(ev.deviceId, a) && !deviceHasAxis(ev.deviceId, b) &&
|
||||
!deviceHasAxis(ev.deviceId, c))
|
||||
return -1f
|
||||
return maxOf(
|
||||
maxOf(ev.getAxisValue(a), ev.getAxisValue(b)),
|
||||
if (c >= 0) ev.getAxisValue(c) else 0f,
|
||||
).coerceIn(0f, 1f)
|
||||
}
|
||||
|
||||
/** The keycode a trigger stands in for. The binding model is keyed on keycodes and most
|
||||
* pads give their triggers none, so every trigger path — capture and gameplay — refers to
|
||||
* them by the code a key-emitting pad would send. */
|
||||
private fun triggerKeyCode(left: Boolean): Int =
|
||||
if (left) KeyEvent.KEYCODE_BUTTON_L2 else KeyEvent.KEYCODE_BUTTON_R2
|
||||
|
||||
private var lastStickProbeMs = 0L
|
||||
private fun debugStickProbe(ev: MotionEvent) {
|
||||
if (!prefs.getBoolean("debug.stickLog", false)) return
|
||||
@@ -4101,6 +4133,14 @@ open class MainActivityRuntime : ComponentActivity() {
|
||||
// here is why its directions could never be bound.
|
||||
val (capRightX, capRightY) = rightStickAxes(ev.deviceId)
|
||||
captureStickCode(ev, capRightX, capRightY, false).takeIf { it != 0 }?.let { want.add(it) }
|
||||
// Analog TRIGGERS, same treatment: on a pad whose triggers are axis-only (an Xbox
|
||||
// controller, and most modern pads) the capture saw nothing at all when one was pulled
|
||||
// — and since this method consumes the motion, not even a UI twitch to explain why.
|
||||
// Standing in the keycode a key-emitting pad would send makes the trigger an ordinary
|
||||
// button downstream, and gameplay resolves that same code back (sendTrigger).
|
||||
for (left in booleanArrayOf(true, false)) {
|
||||
if (triggerTravel(ev, left) > TRIGGER_DIGITAL_THRESHOLD) want.add(triggerKeyCode(left))
|
||||
}
|
||||
captureHatX = dx
|
||||
captureHatY = dy
|
||||
val now = SystemClock.uptimeMillis()
|
||||
@@ -4457,7 +4497,7 @@ open class MainActivityRuntime : ComponentActivity() {
|
||||
// Edge: fire a hotkey with this direction as its MAIN key —
|
||||
// combo-aware (e.g. "hold Select + push R-Stick Up"), falling
|
||||
// back to a plain single-direction binding.
|
||||
ControllerMappings.matchHotkey(code, heldKeys)?.let { runStickHotkey(it) }
|
||||
ControllerMappings.matchHotkey(code, heldKeys)?.let { runEdgeHotkey(it) }
|
||||
}
|
||||
} else {
|
||||
heldKeys.remove(code)
|
||||
@@ -4466,11 +4506,12 @@ open class MainActivityRuntime : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Fire an ARMSX2 hotkey from a non-key source (a CUSTOM stick direction crossing
|
||||
/** Fire an ARMSX2 hotkey from a non-key source (a stick direction or a trigger crossing
|
||||
* its threshold — edge-triggered, treated as a single press). Hold-type hotkeys
|
||||
* (FAST_FORWARD hold, PRESSURE_MOD) are no-ops here — a stick edge has no hold
|
||||
* semantics; the rest mirror the one-shot actions in dispatchKeyEvent. */
|
||||
private fun runStickHotkey(h: ControllerMappings.SysHotkey) {
|
||||
* (FAST_FORWARD hold, PRESSURE_MOD) are no-ops here: a stick edge has no hold semantics,
|
||||
* and sendTrigger handles them itself on both edges. The rest mirror the one-shot
|
||||
* actions in dispatchKeyEvent. */
|
||||
private fun runEdgeHotkey(h: ControllerMappings.SysHotkey) {
|
||||
when (h) {
|
||||
ControllerMappings.SysHotkey.MENU -> InGameOverlay.toggle()
|
||||
ControllerMappings.SysHotkey.SCREENSHOT -> com.armsx2.Screenshots.capture(applicationContext)
|
||||
@@ -4527,7 +4568,7 @@ open class MainActivityRuntime : ComponentActivity() {
|
||||
ControllerMappings.hotkeyForStickCode(code)?.let { hk ->
|
||||
val held = stickHotkeyHeld[port]
|
||||
if (mag > STICK_DIGITAL_THRESHOLD) {
|
||||
if (held.add(code)) runStickHotkey(hk)
|
||||
if (held.add(code)) runEdgeHotkey(hk)
|
||||
} else {
|
||||
held.remove(code)
|
||||
}
|
||||
@@ -4674,38 +4715,64 @@ open class MainActivityRuntime : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendTrigger(event: MotionEvent, axisA: Int, axisB: Int, code: Int, port: Int, axisC: Int = -1) {
|
||||
// A pad with NO analog trigger axis at all — a Nintendo Switch Pro Controller, or an
|
||||
// 8BitDo Pro in Switch mode, which enumerates as one (vendor 0x057e) — delivers L2/R2
|
||||
// ONLY as KEYCODE_BUTTON_L2/R2 key events. Its axis list is just X/Y, Z/RZ and the HAT.
|
||||
//
|
||||
// Reading the absent trigger axes yields 0.0, so the lines below wrote "trigger
|
||||
// released" on EVERY motion event. Hold R2 and move the stick and the stick's own
|
||||
// motion event cancelled the held trigger — "R2 and the stick can't be used at the
|
||||
// same time", which kills racing games. Buttons were unaffected because nothing on the
|
||||
// motion path writes them; only L2/R2 have a motion-side writer. Same shape as the
|
||||
// D-pad "last write wins" bug handled in dispatchDpadCombined.
|
||||
//
|
||||
// When the device has none of these axes, leave the key path in sole charge.
|
||||
if (!deviceHasAxis(event.deviceId, axisA) && !deviceHasAxis(event.deviceId, axisB) &&
|
||||
!deviceHasAxis(event.deviceId, axisC))
|
||||
return
|
||||
// Triggers past TRIGGER_DIGITAL_THRESHOLD, per unified pad slot: edge state for
|
||||
// trigger-bound hotkeys, so each press fires once and re-arms on release.
|
||||
private val triggerHotkeyHeld = Array(8) { HashSet<Int>() }
|
||||
|
||||
private fun sendTrigger(event: MotionEvent, left: Boolean, port: Int) {
|
||||
// -1 = no trigger axis on this side; its L2/R2 is a key event, key path owns it.
|
||||
val raw = triggerTravel(event, left)
|
||||
if (raw < 0f) return
|
||||
val code = triggerKeyCode(left)
|
||||
val held = triggerHotkeyHeld[port]
|
||||
val pressed = raw > TRIGGER_DIGITAL_THRESHOLD
|
||||
|
||||
// Mirror into heldKeys so a held trigger can be a combo MODIFIER, exactly as it is on a
|
||||
// pad whose triggers send key events. Cleared on OUR release edge only — a pad that
|
||||
// reports its triggers both ways must not have the key path's hold wiped by a motion
|
||||
// event that happens to read the axis low.
|
||||
if (pressed) heldKeys.add(code)
|
||||
if (pressed != held.contains(code)) {
|
||||
if (pressed) held.add(code) else { held.remove(code); heldKeys.remove(code) }
|
||||
// Triggers now reach the Hotkeys tab's capture like any other button, so they have
|
||||
// to be able to fire one here. Hold-type hotkeys act on both edges (a trigger has a
|
||||
// real release, unlike a stick edge); the rest fire on the press. Matching on
|
||||
// release re-adds the code, as the key path does, so a combo still resolves.
|
||||
ControllerMappings.matchHotkey(code, if (pressed) heldKeys else heldKeys + code)?.let { hk ->
|
||||
when (hk) {
|
||||
ControllerMappings.SysHotkey.FAST_FORWARD -> {
|
||||
if (pressed) fastForwardToggleActive = false
|
||||
runCatching {
|
||||
NativeApp.speedhackLimitermode(if (pressed) ffLimiterMode() else baseLimiterMode())
|
||||
}
|
||||
}
|
||||
ControllerMappings.SysHotkey.PRESSURE_MOD ->
|
||||
com.armsx2.ui.touch.TouchControls.pressureModifierHeld.value = pressed
|
||||
ControllerMappings.SysHotkey.GYRO_HOLD -> gyroActive.value = pressed
|
||||
else -> if (pressed) runEdgeHotkey(hk)
|
||||
}
|
||||
}
|
||||
// Macros are keyed on the physical code too, and the Pad tab now lets a trigger be
|
||||
// captured for one. Same both-edges firing as dispatchGameplayKey.
|
||||
com.armsx2.ui.touch.TouchControls.macroForPhysicalCode(code)?.let { macro ->
|
||||
com.armsx2.ui.touch.TouchControls.fireMacro(macro, "pad$port", pressed) { c, p ->
|
||||
sendKeyAction(if (p) KeyEventType.KeyDown else KeyEventType.KeyUp, c, port)
|
||||
}
|
||||
}
|
||||
}
|
||||
// A trigger bound to a hotkey or a macro doesn't also drive the pad — the precedence
|
||||
// the key path and emitCustom already apply. The hotkey match is combo-aware, so a
|
||||
// trigger that is merely a MODIFIER keeps working as L2/R2.
|
||||
if (ControllerMappings.matchHotkey(code, heldKeys) != null) return
|
||||
if (com.armsx2.ui.touch.TouchControls.macroForPhysicalCode(code) != null) return
|
||||
|
||||
// Pads report L2/R2 on AXIS_*TRIGGER or on AXIS_BRAKE/GAS — take the higher of
|
||||
// the two, clamping negatives (some non-Xbox pads idle an unused trigger axis at
|
||||
// -1). Then apply the SMALL trigger deadzone and re-normalize the remaining range
|
||||
// to 0..1, so pressure ramps smoothly from zero to full instead of flicking on/off
|
||||
// around the old hard 15% stick-deadzone boundary (the jitter non-Xbox pads showed)
|
||||
// — and the low 15% of travel is no longer wasted.
|
||||
// Honor the L2/R2 binding: triggers arrive as motion axes, never through the
|
||||
// keycode binding path, so clearing/remapping them in the Pad tab was ignored.
|
||||
// Resolve the physical trigger keycode to its mapped PS2 target — null = cleared,
|
||||
// so the trigger is disabled; otherwise drive the resolved (possibly remapped) code.
|
||||
val target = ControllerMappings.targetForPhysical(code, port) ?: return
|
||||
val raw = maxOf(
|
||||
maxOf(event.getAxisValue(axisA), event.getAxisValue(axisB)),
|
||||
if (axisC >= 0) event.getAxisValue(axisC) else 0f,
|
||||
).coerceIn(0f, 1f)
|
||||
// Deadzone off the bottom, re-normalized, so pressure ramps from zero instead of
|
||||
// flicking on/off at a hard threshold (the jitter non-Xbox pads showed).
|
||||
val out = if (raw <= TRIGGER_DEAD) 0f else (raw - TRIGGER_DEAD) / (1f - TRIGGER_DEAD)
|
||||
if (target in 110..123) {
|
||||
// Trigger bound to a PS2 STICK direction ("(send)" rows): contribute the
|
||||
|
||||
@@ -20,6 +20,9 @@ endif()
|
||||
# GS vertex front-end kernel oracle (arch-neutral).
|
||||
add_subdirectory(gs)
|
||||
|
||||
# IPU colour-conversion oracle (arch-neutral: each host tests its own path).
|
||||
add_subdirectory(ipu)
|
||||
|
||||
|
||||
set(multi_isa_sources
|
||||
GS/swizzle_test_main.cpp
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# IPU colour-conversion oracle suite. ipu_dither() compiles to a different
|
||||
# implementation per architecture and the three were never compared to each other;
|
||||
# these hold whichever one this host selected to the scalar reference.
|
||||
add_pcsx2_test(ipu_dither_tests
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../StubHost.cpp
|
||||
ipu_dither_tests.cpp
|
||||
)
|
||||
|
||||
target_include_directories(ipu_dither_tests PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../../../../pcsx2
|
||||
)
|
||||
|
||||
target_link_libraries(ipu_dither_tests PUBLIC
|
||||
PCSX2_FLAGS
|
||||
PCSX2
|
||||
common
|
||||
)
|
||||
@@ -0,0 +1,204 @@
|
||||
// SPDX-FileCopyrightText: 2026 ARMSX2 Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0+
|
||||
|
||||
// Whichever dither path this host selects must be bit-identical to the scalar one.
|
||||
//
|
||||
// ipu_dither() has three implementations -- a scalar reference, an SSE2 path and a
|
||||
// NEON path -- and the emulator picks one at compile time from the architecture.
|
||||
// Nothing ever compared them. That is a bad shape for a function like this: it is
|
||||
// the last step of MPEG colour conversion, so an error does not crash, it tints an
|
||||
// FMV slightly, and nobody files that as a bug.
|
||||
//
|
||||
// The transform is per-pixel and depends on nothing but the pixel's four bytes and
|
||||
// its position modulo four in each axis, so the whole input domain is small enough
|
||||
// to cover directly rather than sampled. The sweeps below walk every byte value
|
||||
// through every one of the sixteen dither cells; the randomised case exists on top
|
||||
// of that only to catch a path that crosses channels, which a sweep holding r, g
|
||||
// and b equal would not see.
|
||||
//
|
||||
// Both dither states matter. dte=0 is not a trivial passthrough -- it still packs
|
||||
// 8888 down to 1555 and still derives alpha from a compare against 0x40 -- so a
|
||||
// path that only got the dithered arm right would be half broken in exactly the
|
||||
// mode most games use.
|
||||
|
||||
#include "IPU/IPU_MultiISA.h"
|
||||
#include "GS/MultiISA.h"
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <random>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int kDim = 16;
|
||||
|
||||
// A value no real conversion can produce: every output pixel has its top bit
|
||||
// set only when alpha matched, so an all-ones block would require every pixel
|
||||
// to be white and opaque at once. Seeding with it turns "this path skipped a
|
||||
// pixel" into a mismatch instead of a silent pass on stale memory.
|
||||
void PoisonOutput(macroblock_rgb16& rgb16)
|
||||
{
|
||||
std::memset(&rgb16, 0xFF, sizeof(rgb16));
|
||||
}
|
||||
|
||||
// Returns the index of the first differing pixel, or -1 when the two agree.
|
||||
int FirstMismatch(const macroblock_rgb16& a, const macroblock_rgb16& b)
|
||||
{
|
||||
u16 a_words[kDim * kDim];
|
||||
u16 b_words[kDim * kDim];
|
||||
std::memcpy(a_words, &a, sizeof(a_words));
|
||||
std::memcpy(b_words, &b, sizeof(b_words));
|
||||
|
||||
for (int i = 0; i < kDim * kDim; i++)
|
||||
{
|
||||
if (a_words[i] != b_words[i])
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Runs the host's selected path and the reference over the same input and
|
||||
// reports the first pixel they disagree on, with enough context to place it in
|
||||
// the dither matrix.
|
||||
void ExpectMatchesReference(const macroblock_rgb32& rgb32, int dte, const char* what)
|
||||
{
|
||||
macroblock_rgb16 got;
|
||||
macroblock_rgb16 want;
|
||||
PoisonOutput(got);
|
||||
PoisonOutput(want);
|
||||
|
||||
MULTI_ISA_SELECT(ipu_dither)(rgb32, got, dte);
|
||||
MULTI_ISA_SELECT(ipu_dither_reference)(rgb32, want, dte);
|
||||
|
||||
const int bad = FirstMismatch(got, want);
|
||||
if (bad < 0)
|
||||
return;
|
||||
|
||||
const int row = bad / kDim;
|
||||
const int col = bad % kDim;
|
||||
const auto& src = rgb32.c[row][col];
|
||||
|
||||
u16 got_words[kDim * kDim];
|
||||
u16 want_words[kDim * kDim];
|
||||
std::memcpy(got_words, &got, sizeof(got_words));
|
||||
std::memcpy(want_words, &want, sizeof(want_words));
|
||||
|
||||
ADD_FAILURE() << what << ": dte=" << dte << " first mismatch at row " << row
|
||||
<< " col " << col << " (dither cell [" << (row & 3) << "][" << (col & 3) << "])"
|
||||
<< "\n source rgba = " << int(src.r) << ", " << int(src.g) << ", "
|
||||
<< int(src.b) << ", " << int(src.a)
|
||||
<< "\n got = 0x" << std::hex << got_words[bad]
|
||||
<< "\n want = 0x" << want_words[bad] << std::dec;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Every byte value, through every dither cell, on the colour channels. Holding the
|
||||
// three channels equal is what makes this a clean sweep of the dither arithmetic;
|
||||
// channel independence is the randomised test's job.
|
||||
TEST(IPUDither, ColourSweepMatchesReference)
|
||||
{
|
||||
for (int v = 0; v <= 255; v++)
|
||||
{
|
||||
macroblock_rgb32 rgb32;
|
||||
for (int i = 0; i < kDim; i++)
|
||||
{
|
||||
for (int j = 0; j < kDim; j++)
|
||||
{
|
||||
rgb32.c[i][j].r = static_cast<u8>(v);
|
||||
rgb32.c[i][j].g = static_cast<u8>(v);
|
||||
rgb32.c[i][j].b = static_cast<u8>(v);
|
||||
// Alternate the two alpha outcomes so neither is ever untested.
|
||||
rgb32.c[i][j].a = ((i + j) & 1) ? 0x40 : 0x00;
|
||||
}
|
||||
}
|
||||
|
||||
ExpectMatchesReference(rgb32, 1, "colour sweep");
|
||||
ExpectMatchesReference(rgb32, 0, "colour sweep");
|
||||
}
|
||||
}
|
||||
|
||||
// Alpha is a compare against 0x40, not a range, so the interesting inputs are the
|
||||
// neighbours of that value as much as the extremes. Sweeping the whole byte covers
|
||||
// both without having to guess.
|
||||
TEST(IPUDither, AlphaSweepMatchesReference)
|
||||
{
|
||||
for (int v = 0; v <= 255; v++)
|
||||
{
|
||||
macroblock_rgb32 rgb32;
|
||||
for (int i = 0; i < kDim; i++)
|
||||
{
|
||||
for (int j = 0; j < kDim; j++)
|
||||
{
|
||||
// Distinct per channel, so an alpha bug cannot hide behind a
|
||||
// colour that happens to match.
|
||||
rgb32.c[i][j].r = static_cast<u8>(j * 16);
|
||||
rgb32.c[i][j].g = static_cast<u8>(i * 16);
|
||||
rgb32.c[i][j].b = static_cast<u8>((i + j) * 8);
|
||||
rgb32.c[i][j].a = static_cast<u8>(v);
|
||||
}
|
||||
}
|
||||
|
||||
ExpectMatchesReference(rgb32, 1, "alpha sweep");
|
||||
ExpectMatchesReference(rgb32, 0, "alpha sweep");
|
||||
}
|
||||
}
|
||||
|
||||
// The saturating arithmetic only shows its edges where a cell pushes a value past
|
||||
// a limit, and the cells reach +3 and -4. Pinning the exact boundary values means a
|
||||
// path that clamps with the wrong operation fails here rather than on one unlucky
|
||||
// random block.
|
||||
TEST(IPUDither, SaturationEdgesMatchReference)
|
||||
{
|
||||
static constexpr std::array<u8, 10> kEdges = {0, 1, 2, 3, 4, 251, 252, 253, 254, 255};
|
||||
|
||||
for (const u8 lo : kEdges)
|
||||
{
|
||||
for (const u8 hi : kEdges)
|
||||
{
|
||||
macroblock_rgb32 rgb32;
|
||||
for (int i = 0; i < kDim; i++)
|
||||
{
|
||||
for (int j = 0; j < kDim; j++)
|
||||
{
|
||||
rgb32.c[i][j].r = lo;
|
||||
rgb32.c[i][j].g = hi;
|
||||
rgb32.c[i][j].b = static_cast<u8>((j & 1) ? lo : hi);
|
||||
rgb32.c[i][j].a = ((i + j) & 1) ? 0x40 : 0x3F;
|
||||
}
|
||||
}
|
||||
|
||||
ExpectMatchesReference(rgb32, 1, "saturation edges");
|
||||
ExpectMatchesReference(rgb32, 0, "saturation edges");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The sweeps all hold something constant across the block. This one holds nothing
|
||||
// constant, which is what catches a path that reads the right bytes into the wrong
|
||||
// channel -- a deinterleave that transposes r and b survives every test above.
|
||||
TEST(IPUDither, RandomMacroblocksMatchReference)
|
||||
{
|
||||
std::mt19937 rng(20260814u); // fixed seed: a failing case must be reproducible
|
||||
std::uniform_int_distribution<int> byte(0, 255);
|
||||
|
||||
for (int iter = 0; iter < 256; iter++)
|
||||
{
|
||||
macroblock_rgb32 rgb32;
|
||||
for (int i = 0; i < kDim; i++)
|
||||
{
|
||||
for (int j = 0; j < kDim; j++)
|
||||
{
|
||||
rgb32.c[i][j].r = static_cast<u8>(byte(rng));
|
||||
rgb32.c[i][j].g = static_cast<u8>(byte(rng));
|
||||
rgb32.c[i][j].b = static_cast<u8>(byte(rng));
|
||||
// Bias alpha towards the one value the compare cares about,
|
||||
// otherwise it is almost never hit at random.
|
||||
rgb32.c[i][j].a = (byte(rng) < 128) ? 0x40 : static_cast<u8>(byte(rng));
|
||||
}
|
||||
}
|
||||
|
||||
ExpectMatchesReference(rgb32, iter & 1, "random macroblock");
|
||||
}
|
||||
}
|
||||
@@ -145,10 +145,14 @@ u32 Obs(int id, const char* name)
|
||||
// process. Elsewhere the two callers skip.
|
||||
//
|
||||
// The candidates are 4K-aligned but none is 16K-aligned, so on a 16K-page
|
||||
// kernel — Asahi, Apple Silicon, some Android — every one is rejected outright
|
||||
// and the two callers always skip. Re-picking them 16K-aligned would need the
|
||||
// tag/index constraints re-derived against the console capture, so it is left
|
||||
// to whoever holds that data.
|
||||
// kernel — Asahi, Apple Silicon, some Android — every one is rejected outright.
|
||||
// Re-picking them 16K-aligned would need the tag/index constraints re-derived
|
||||
// against the console capture, so it is left to whoever holds that data.
|
||||
//
|
||||
// A null return is therefore routine, not exceptional, and callers should treat
|
||||
// the mapping as an optional negative control rather than a precondition. Only
|
||||
// DxstgDirtyStaysInsideGuestMemory is wholly about the host page and has to
|
||||
// skip; the write-back check keeps its guest-side half running everywhere.
|
||||
void* MapAt(u32* chosen)
|
||||
{
|
||||
#if defined(MAP_FIXED_NOREPLACE)
|
||||
@@ -439,14 +443,22 @@ TEST(EeCache2Console, DxstgWriteBackTargetsTheTaggedGuestPage)
|
||||
constexpr u32 kTargetPage = 0x00129000;
|
||||
constexpr u32 kTarget = kTargetPage + kSetIndex * 64;
|
||||
|
||||
// The host mapping is only the negative control: proof that the write-back
|
||||
// did not ALSO reach the host page that happens to carry the same number.
|
||||
// Everything else here is guest-side and needs nothing from the host, so it
|
||||
// runs unconditionally. On a 16K-page kernel -- Asahi, Apple Silicon, some
|
||||
// Android -- MapAt cannot honour a 4K-aligned request and returns null;
|
||||
// only the control is skipped then, not the whole test.
|
||||
u32 page = 0;
|
||||
void* p = MapAt(&page);
|
||||
if (!p)
|
||||
GTEST_SKIP() << "could not map a page at any candidate host address";
|
||||
ASSERT_EQ(page, kTargetPage) << "the control page moved; re-point kTargetPage";
|
||||
std::memset(p, 0xEE, 0x1000);
|
||||
const u32* host = reinterpret_cast<const u32*>(
|
||||
static_cast<uptr>(page) + kSetIndex * 64);
|
||||
const u32* host = nullptr;
|
||||
if (p)
|
||||
{
|
||||
ASSERT_EQ(page, kTargetPage) << "the control page moved; re-point kTargetPage";
|
||||
std::memset(p, 0xEE, 0x1000);
|
||||
host = reinterpret_cast<const u32*>(
|
||||
static_cast<uptr>(page) + kSetIndex * 64);
|
||||
}
|
||||
|
||||
{
|
||||
EeRecTestHarness h;
|
||||
@@ -463,7 +475,8 @@ TEST(EeCache2Console, DxstgWriteBackTargetsTheTaggedGuestPage)
|
||||
// 64 bytes of guest cache line, at the guest physical page the tag names.
|
||||
EXPECT_EQ(memRead32(kTarget), 0x5A5A0009u);
|
||||
EXPECT_EQ(memRead32(kTarget + 4), 0xDEADBEEFu);
|
||||
EXPECT_EQ(host[0], 0xEEEEEEEEu) << "the write-back still reaches a host address";
|
||||
if (host)
|
||||
EXPECT_EQ(host[0], 0xEEEEEEEEu) << "the write-back still reaches a host address";
|
||||
}
|
||||
|
||||
// Never filled, and filled-then-invalidated. Both used to be declined
|
||||
@@ -471,7 +484,8 @@ TEST(EeCache2Console, DxstgWriteBackTargetsTheTaggedGuestPage)
|
||||
for (const bool invalidate_first : {false, true})
|
||||
{
|
||||
SCOPED_TRACE(invalidate_first ? "filled then invalidated" : "never filled");
|
||||
std::memset(p, 0xEE, 0x1000);
|
||||
if (p)
|
||||
std::memset(p, 0xEE, 0x1000);
|
||||
EeRecTestHarness h;
|
||||
resetCache();
|
||||
memWrite32(kTarget, 0xA5A5A5A5u);
|
||||
@@ -489,27 +503,52 @@ TEST(EeCache2Console, DxstgWriteBackTargetsTheTaggedGuestPage)
|
||||
RunCacheOp(0x12, kProbeLine);
|
||||
RunCacheOp(0x14, kProbeLine);
|
||||
EXPECT_EQ(memRead32(kTarget), 0u) << "the cleared line did not write back";
|
||||
EXPECT_EQ(host[0], 0xEEEEEEEEu) << "the write-back still reaches a host address";
|
||||
if (host)
|
||||
EXPECT_EQ(host[0], 0xEEEEEEEEu) << "the write-back still reaches a host address";
|
||||
}
|
||||
|
||||
munmap(p, 0x1000);
|
||||
if (p)
|
||||
munmap(p, 0x1000);
|
||||
}
|
||||
|
||||
// A DXSTG naming a page that does not resolve to plain guest memory leaves the
|
||||
// line unbacked, so the write-back declines rather than dereferencing anything.
|
||||
// 0x1FC00000-and-up is BIOS/unmapped territory at the top of the physical map.
|
||||
//
|
||||
// 0x60129000 is past the end of the physical map, and it is the page with
|
||||
// teeth: the lookup used to keep only the low 29 bits of the tag, so this page
|
||||
// folded onto 0x00129000 -- ordinary main RAM -- and wrote sixty-four bytes
|
||||
// there. The witness turns "we did not fault" into "we did not write somewhere
|
||||
// the guest never named". An SCPH-30001 agrees on that much: an eviction
|
||||
// steered above the end of RAM puts nothing into RAM.
|
||||
//
|
||||
// This check used to name 0x1FFFF000, calling it unmapped. That is the last
|
||||
// page of the 4 MB BIOS ROM at 0x1FC00000, so it is real backing memory and the
|
||||
// declining branch never ran at all.
|
||||
TEST(EeCache2Console, DxstgOnAnUnresolvablePageDeclinesTheWriteBack)
|
||||
{
|
||||
constexpr u32 kUnresolvablePage = 0x60129000u;
|
||||
constexpr u32 kWitness = 0x00129000u + kSetIndex * 64;
|
||||
|
||||
EeRecTestHarness h;
|
||||
resetCache();
|
||||
memWrite32(kWitness, 0xA5A5A5A5u);
|
||||
writeCache32(kProbeLine, 0x5A5A0009u);
|
||||
cpuRegs.CP0.n.TagLo = 0x1FFFF000u | kFlagDirty | kFlagValid;
|
||||
cpuRegs.CP0.n.TagLo = kUnresolvablePage | kFlagDirty | kFlagValid;
|
||||
RunCacheOp(0x12, kProbeLine); // DXSTG
|
||||
RunCacheOp(0x14, kProbeLine); // DXWBIN -- must be a no-op, not a store
|
||||
// Reaching here without a fault is the assertion; the flags still round-trip.
|
||||
|
||||
EXPECT_EQ(memRead32(kWitness), 0xA5A5A5A5u)
|
||||
<< "the write-back reached guest memory the tag never named";
|
||||
EXPECT_EQ(ReadTag(kProbeLine) & (kFlagValid | kFlagDirty), 0u);
|
||||
}
|
||||
|
||||
// Deliberately unpinned: where an eviction goes when the tag names one of the
|
||||
// emulator's main-RAM mirrors at 0x20000000 or 0x30000000. Those are our
|
||||
// physical map's mirrors, not the console's -- a console has no RAM at those
|
||||
// physical addresses, and an eviction aimed there reached nothing on the
|
||||
// SCPH-30001. Any assertion here would freeze an emulator-specific answer to a
|
||||
// question no game asks, so leave it undefined.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tripwires. DxstgDirtyStaysInsideGuestMemory has graduated and holds; the rest
|
||||
// fail today and turn green when the missing model appears.
|
||||
|
||||
Reference in New Issue
Block a user