mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Merge origin/master into integrate/game-codegen-hooks-into-rollback
Bring upstream hotfixes (DualShock rumble, load-delay, BIOS pin, mods) into the integrate branch. Resolve SIO conflicts by keeping multitap / PSX_MAX_PLAYERS paths and appending rumble state after snapshot meta. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -150,14 +150,13 @@ will still see our HYBRID auto-flip yank it back to digital — exactly the kind
|
||||
of "type flips underneath the game" desync the deferred-request machinery was
|
||||
built to avoid but cannot prevent, because we never learned the pad is locked.
|
||||
|
||||
### D7 — **`0x4D` rumble-map is a canned constant, not an echo-back map** (semantic)
|
||||
Beetle's `0x4D` echoes the previous `rumble_magic[]` while latching the new
|
||||
mapping bytes (`dualshock.cpp:993-1024`); the map then routes `0x42` weak/strong
|
||||
rumble params (`dualshock.cpp:617-644`). Ours returns a fixed
|
||||
`{0xF3,0x5A,0,0,0,0,0,0}` (`sio.c:812`/`830`) and implements **no rumble at
|
||||
all.** Acceptable (no rumble output device), but a game that *reads back* the
|
||||
`0x4D` map to confirm rumble init will get the wrong echo and may decide rumble
|
||||
is absent. Low player-visible impact (no rumble hardware anyway).
|
||||
### D7 — **`0x4D` rumble-map negotiation and host output** (resolved)
|
||||
Resolved: `0x4D` now echoes the previous six-byte motor map while latching the
|
||||
new map. Mapped motor bytes from later `0x42` polls are retained per slot and
|
||||
forwarded to SDL's low/high-frequency rumble channels by the frontend. The
|
||||
protocol path is pinned by `sio_dualshock_rumble_test`; legacy savestates remain
|
||||
loadable and receive the conventional `{0x00,0x01,0xFF,...}` map with stopped
|
||||
motors.
|
||||
|
||||
### D8 — **`0x45` analog-status byte is hard-coded `0x03` (analog on)** (semantic)
|
||||
Beetle `0x4501` reports `transmit_buffer[1] = analog_mode ? 0x01 : 0x00`
|
||||
@@ -289,9 +288,8 @@ every title — which is exactly what the LEGACY comment predicted, letting the
|
||||
The flag, legacy SIO branches, config key, debug command, helper script, and
|
||||
Tomba opt-in were removed. All titles use the modern DualShock state machine.
|
||||
|
||||
5. **[P2] Echo-back `0x4D` rumble map** (D7). Even with no rumble output,
|
||||
echo the previous `rumble_magic[]` bytes (`dualshock.cpp:993-1024`) so a game
|
||||
that confirms rumble-init reads sane values. Low visible impact.
|
||||
5. **[DONE 2026-08-06] Echo-back `0x4D` rumble map and forward motors.** The
|
||||
SIO model now routes negotiated weak/strong values to the SDL frontend.
|
||||
|
||||
6. **[P2] Move pad ACK/IRQ timing onto the cycle-paced shifter** (D1). Remove
|
||||
the pad fast-path bypass (`sio.c:1386`) so pad bytes use
|
||||
|
||||
@@ -1758,6 +1758,69 @@ std::string CodeGenerator::translate_basic_block(
|
||||
exit_branch_addr = block.end_addr;
|
||||
}
|
||||
|
||||
/* MIPS-I load-delay value semantics for ordinary in-block pairs. The
|
||||
* resident/full-function emitter already models these; overlay/game CFG
|
||||
* emission must do the same. OpenBIOS's CD/card fasttrack deliberately
|
||||
* uses `lw k0,...; move at,k0`, where the move must observe OLD k0. */
|
||||
auto simple_load_dest = [](uint32_t w) -> int {
|
||||
uint32_t op = w >> 26;
|
||||
if (op == 0x20u || op == 0x21u || op == 0x23u ||
|
||||
op == 0x24u || op == 0x25u) {
|
||||
uint32_t rt = (w >> 16) & 31u;
|
||||
return rt ? static_cast<int>(rt) : -1;
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
auto reads_gpr = [](uint32_t w, uint32_t r) -> bool {
|
||||
if (r == 0u || w == 0u) return false;
|
||||
uint32_t op = w >> 26, rs = (w >> 21) & 31u;
|
||||
uint32_t rt = (w >> 16) & 31u, fn = w & 63u;
|
||||
if (op == 0u) {
|
||||
if (fn == 0x08u || fn == 0x09u) return rs == r;
|
||||
if (fn == 0x0Cu || fn == 0x0Du || fn == 0x10u || fn == 0x12u)
|
||||
return false;
|
||||
return rs == r || rt == r;
|
||||
}
|
||||
if (op == 0x02u || op == 0x03u || op == 0x0Fu) return false;
|
||||
if (op == 0x01u || op == 0x06u || op == 0x07u) return rs == r;
|
||||
if (op == 0x04u || op == 0x05u) return rs == r || rt == r;
|
||||
if (op >= 0x08u && op <= 0x0Eu) return rs == r;
|
||||
if (op == 0x10u) return ((w >> 21) & 31u) == 0x04u && rt == r;
|
||||
if (op == 0x12u) {
|
||||
uint32_t f = (w >> 21) & 31u;
|
||||
return (f == 0x04u || f == 0x06u) && rt == r;
|
||||
}
|
||||
if (op == 0x22u || op == 0x26u ||
|
||||
(op >= 0x28u && op <= 0x2Eu))
|
||||
return rs == r || rt == r;
|
||||
return rs == r;
|
||||
};
|
||||
auto writes_gpr = [](uint32_t w, uint32_t r) -> bool {
|
||||
if (r == 0u || w == 0u) return false;
|
||||
uint32_t op = w >> 26, rt = (w >> 16) & 31u;
|
||||
uint32_t rd = (w >> 11) & 31u, fn = w & 63u;
|
||||
if (op == 0u) {
|
||||
if (fn == 0x08u || fn == 0x0Cu || fn == 0x0Du || fn == 0x0Fu ||
|
||||
fn == 0x11u || fn == 0x13u ||
|
||||
(fn >= 0x18u && fn <= 0x1Bu)) return false;
|
||||
return rd == r;
|
||||
}
|
||||
if (op == 0x03u) return r == 31u;
|
||||
if (op == 0x01u) return (rt == 0x10u || rt == 0x11u) && r == 31u;
|
||||
if (op == 0x02u || (op >= 0x04u && op <= 0x07u) ||
|
||||
(op >= 0x28u && op <= 0x2Eu) || op == 0x32u || op == 0x3Au)
|
||||
return false;
|
||||
if (op == 0x10u) return ((w >> 21) & 31u) == 0u && rt == r;
|
||||
if (op == 0x12u) {
|
||||
uint32_t f = (w >> 21) & 31u;
|
||||
return (f == 0u || f == 2u) && rt == r;
|
||||
}
|
||||
return rt == r;
|
||||
};
|
||||
uint32_t delayed_load_addr = 0u;
|
||||
uint32_t delayed_load_dest = 0u;
|
||||
bool delayed_load_active = false;
|
||||
|
||||
while (addr <= block.end_addr) {
|
||||
auto instr_opt = exe_.read_word(addr);
|
||||
if (!instr_opt.has_value()) {
|
||||
@@ -1790,7 +1853,42 @@ std::string CodeGenerator::translate_basic_block(
|
||||
if (!is_cf) {
|
||||
if (cycle_per_insn) emit_pre_icache(addr, config_.indent);
|
||||
if (cycle_per_insn) emit_pre_timing(instr, config_.indent);
|
||||
ss << translate_instruction(addr, instr) << "\n";
|
||||
std::string emitted = translate_instruction(addr, instr);
|
||||
int load_dest = simple_load_dest(instr);
|
||||
bool defer_load = false;
|
||||
if (!delayed_load_active && load_dest > 0 && addr + 4u <= block.end_addr &&
|
||||
!extra_labels_.count(addr + 4u)) {
|
||||
auto next = exe_.read_word(addr + 4u);
|
||||
defer_load = next.has_value() &&
|
||||
!ControlFlowAnalyzer::is_control_flow(*next) &&
|
||||
reads_gpr(*next, static_cast<uint32_t>(load_dest));
|
||||
}
|
||||
if (defer_load) {
|
||||
const std::string lhs = fmt::format("cpu->gpr[{}] =", load_dest);
|
||||
const size_t pos = emitted.find(lhs);
|
||||
if (pos != std::string::npos) {
|
||||
const std::string temp = fmt::format("psx_ldd_{:08X}", addr);
|
||||
emitted.replace(pos, lhs.size(), "uint32_t " + temp + " =");
|
||||
ss << config_.indent << "{ /* MIPS-I load-delay pair */\n";
|
||||
delayed_load_addr = addr;
|
||||
delayed_load_dest = static_cast<uint32_t>(load_dest);
|
||||
delayed_load_active = true;
|
||||
}
|
||||
}
|
||||
ss << emitted << "\n";
|
||||
if (delayed_load_active && addr == delayed_load_addr + 4u) {
|
||||
if (writes_gpr(instr, delayed_load_dest)) {
|
||||
ss << config_.indent << fmt::format(
|
||||
"(void)psx_ldd_{:08X}; /* successor write wins */\n",
|
||||
delayed_load_addr);
|
||||
} else {
|
||||
ss << config_.indent << fmt::format(
|
||||
"cpu->gpr[{}] = psx_ldd_{:08X}; /* load-delay writeback */\n",
|
||||
delayed_load_dest, delayed_load_addr);
|
||||
}
|
||||
ss << config_.indent << "}\n";
|
||||
delayed_load_active = false;
|
||||
}
|
||||
emit_cosim_instr(addr, config_.indent);
|
||||
} else {
|
||||
// Control flow is handled at block exit
|
||||
|
||||
@@ -931,6 +931,38 @@ void jump_table_producer_codegen_test() {
|
||||
"alias regression fixture reaches the table when ownership is absent");
|
||||
}
|
||||
|
||||
void cfg_codegen_load_delay_test() {
|
||||
constexpr uint32_t base = 0x80003590u;
|
||||
PSXRecomp::PS1Executable exe{};
|
||||
exe.header.load_address = base;
|
||||
exe.header.initial_pc = base;
|
||||
exe.header.file_size = 20u;
|
||||
append_word(exe.code_data, 0x8F5A4C38u); // lw k0,0x4c38(k0)
|
||||
append_word(exe.code_data, 0x03400825u); // move at,k0 (must see old k0)
|
||||
append_word(exe.code_data, 0xAC3A4C38u); // sw k0,0x4c38(at)
|
||||
append_word(exe.code_data, 0x03E00008u); // jr ra
|
||||
append_word(exe.code_data, 0x00000000u); // delay-slot nop
|
||||
|
||||
PSXRecomp::Function function{};
|
||||
function.start_addr = base;
|
||||
function.end_addr = base + 20u;
|
||||
function.size = 20u;
|
||||
function.name = "cfg_load_delay";
|
||||
PSXRecomp::ControlFlowAnalyzer analyzer(exe);
|
||||
const auto cfg = analyzer.analyze_function(function);
|
||||
PSXRecomp::CodeGenerator generator(exe);
|
||||
const std::string code = generator.generate_function(function, cfg).full_code;
|
||||
|
||||
const size_t deferred = code.find("uint32_t psx_ldd_80003590 =");
|
||||
const size_t successor = code.find("cpu->gpr[1] = cpu->gpr[26]");
|
||||
const size_t writeback = code.find(
|
||||
"cpu->gpr[26] = psx_ldd_80003590; /* load-delay writeback */");
|
||||
check(deferred != std::string::npos && successor != std::string::npos &&
|
||||
writeback != std::string::npos && deferred < successor &&
|
||||
successor < writeback,
|
||||
"CFG codegen preserves MIPS-I dependent load-delay value semantics");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
@@ -945,6 +977,7 @@ int main() {
|
||||
codegen_tests();
|
||||
gte_codegen_classification_tests();
|
||||
jump_table_producer_codegen_test();
|
||||
cfg_codegen_load_delay_test();
|
||||
} catch (const std::exception& e) {
|
||||
fmt::print(stderr, "FAIL unexpected exception: {}\n", e.what());
|
||||
++failures;
|
||||
|
||||
@@ -52,6 +52,12 @@ if(BUILD_TESTING)
|
||||
PSX_OVERLAY_DLL_BUILD=1)
|
||||
add_test(NAME psx_icache_fastpath_test COMMAND psx_icache_fastpath_test)
|
||||
|
||||
add_executable(sio_dualshock_rumble_test
|
||||
tests/test_sio_dualshock_rumble.c
|
||||
src/sio.c)
|
||||
target_include_directories(sio_dualshock_rumble_test PRIVATE include)
|
||||
add_test(NAME sio_dualshock_rumble_test COMMAND sio_dualshock_rumble_test)
|
||||
|
||||
add_executable(gte_register_access_test
|
||||
tests/test_gte_register_access.cpp
|
||||
src/gte.cpp
|
||||
|
||||
@@ -146,6 +146,12 @@ struct ModOverlay {
|
||||
std::string expected_sha256;
|
||||
uint64_t size = 0;
|
||||
std::map<std::string, std::string> when;
|
||||
struct FeaturePredicate {
|
||||
bool present = false;
|
||||
std::string package_id;
|
||||
std::string feature_id;
|
||||
bool enabled = true;
|
||||
} when_feature;
|
||||
int64_t order = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@ extern "C" {
|
||||
/* Called before a guest dispatch. Applies the complete main-EXE plan
|
||||
* transactionally on the first dispatch to the configured entry point. */
|
||||
void mod_runtime_on_dispatch(uint32_t target);
|
||||
/* A full-machine savestate restores guest RAM after the initial entry-point
|
||||
* application. Reapply the already-validated main-EXE plan so the current
|
||||
* enabled mod selection remains authoritative after the restore. */
|
||||
void mod_runtime_on_savestate_loaded(void);
|
||||
/* Invokes activation callbacks for the committed plan. Call after the final
|
||||
* launcher commit and before renderer/window initialization. */
|
||||
void mod_runtime_activate_plugins(void);
|
||||
|
||||
@@ -132,6 +132,12 @@ void sio_set_pad_connected(int slot, int connected);
|
||||
* analog/hybrid behaviour is unchanged. */
|
||||
void sio_set_pad_config_capable(int slot, int capable);
|
||||
|
||||
/* Read the raw DualShock motor state selected by the guest's 0x4D rumble map
|
||||
* and most recent 0x42 poll. `small` is the fixed-strength high-frequency
|
||||
* motor byte; `large` is the variable-strength low-frequency motor byte.
|
||||
* The frontend translates these values to its host controller API. */
|
||||
void sio_get_pad_rumble(int slot, uint8_t *small, uint8_t *large);
|
||||
|
||||
/* Return current pad button state (for debug server). _slot targets a logical
|
||||
* pad 0 .. PSX_MAX_PLAYERS-1. */
|
||||
uint16_t sio_get_pad_buttons(void);
|
||||
|
||||
+21
-1
@@ -623,10 +623,30 @@ function(psxrecomp_add_runtime_target target)
|
||||
endif()
|
||||
|
||||
if(PSXRT_BIOS_GENERATED_FULL_C AND PSXRT_BIOS_GENERATED_DISPATCH_C)
|
||||
set(generated_sources
|
||||
# Per-game BIOS pin: the pinned files REPLACE the matching stem's
|
||||
# fork-global generated files only. Every other backend and the
|
||||
# generated psx_bios_registry.c stay linked — replacing the whole set
|
||||
# (the previous behaviour) dropped the registry and the other
|
||||
# backends, producing undefined references to psx_bios_registry at
|
||||
# link time. The registry's extern for the pinned stem is satisfied
|
||||
# by the pinned dispatch.c, which carries the same backend descriptor.
|
||||
get_filename_component(_psxrt_pin_name "${PSXRT_BIOS_GENERATED_DISPATCH_C}" NAME)
|
||||
string(REPLACE "_dispatch.c" "" _psxrt_pin_stem "${_psxrt_pin_name}")
|
||||
set(generated_sources "")
|
||||
foreach(_psxrt_src IN LISTS PSXRECOMP_BIOS_GENERATED)
|
||||
get_filename_component(_psxrt_src_name "${_psxrt_src}" NAME)
|
||||
if(_psxrt_src_name STREQUAL "${_psxrt_pin_stem}_full.c" OR
|
||||
_psxrt_src_name STREQUAL "${_psxrt_pin_stem}_dispatch.c")
|
||||
# replaced by the game's pinned copy
|
||||
else()
|
||||
list(APPEND generated_sources "${_psxrt_src}")
|
||||
endif()
|
||||
endforeach()
|
||||
list(APPEND generated_sources
|
||||
"${PSXRT_BIOS_GENERATED_FULL_C}"
|
||||
"${PSXRT_BIOS_GENERATED_DISPATCH_C}")
|
||||
set_source_files_properties(${generated_sources} PROPERTIES GENERATED TRUE)
|
||||
message(STATUS "psxrecomp: BIOS stem ${_psxrt_pin_stem} pinned to game-local copies")
|
||||
else()
|
||||
set(generated_sources ${PSXRECOMP_BIOS_GENERATED})
|
||||
endif()
|
||||
|
||||
@@ -326,6 +326,10 @@ struct PlayerInput {
|
||||
int deadzone = 3277; /* raw SDL axis units, ~10% default */
|
||||
SDL_GameController* handle = nullptr;
|
||||
SDL_JoystickID instance = -1;
|
||||
uint8_t rumble_small = 0;
|
||||
uint8_t rumble_large = 0;
|
||||
bool rumble_known = false;
|
||||
bool rumble_warned = false;
|
||||
};
|
||||
static PlayerInput g_players[PSX_MAX_PLAYERS];
|
||||
/* Offline SIO sample loop bound (from game.toml players; clamped). */
|
||||
@@ -883,6 +887,7 @@ extern "C" void psx_frontend_on_savestate_notify(int is_load, int slot, int ok)
|
||||
}
|
||||
|
||||
extern "C" void psx_frontend_on_savestate_loaded(void) {
|
||||
mod_runtime_on_savestate_loaded();
|
||||
s_disabled_frame_presented = false;
|
||||
s_force_present_after_load = true;
|
||||
smooth_60_reset();
|
||||
@@ -3384,10 +3389,16 @@ static void load_input_config(const char* argv0) {
|
||||
|
||||
static void close_player(PlayerInput& p) {
|
||||
if (p.handle) {
|
||||
if (p.rumble_small || p.rumble_large)
|
||||
(void)SDL_GameControllerRumble(p.handle, 0, 0, 0);
|
||||
SDL_GameControllerClose(p.handle);
|
||||
p.handle = nullptr;
|
||||
p.instance = -1;
|
||||
}
|
||||
p.rumble_small = 0;
|
||||
p.rumble_large = 0;
|
||||
p.rumble_known = false;
|
||||
p.rumble_warned = false;
|
||||
}
|
||||
|
||||
static void close_controller(void) {
|
||||
@@ -3440,6 +3451,61 @@ static void open_player(PlayerInput& p, int self_slot) {
|
||||
const char* name = SDL_GameControllerName(p.handle);
|
||||
std::fprintf(stdout, "psxrecomp runtime: opened controller for slot: %s\n",
|
||||
name ? name : "(unnamed)");
|
||||
p.rumble_known = false;
|
||||
p.rumble_warned = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Forward the emulated DualShock's two motors to the SDL controller assigned
|
||||
* to the same PSX port. The large motor is variable-strength/low-frequency;
|
||||
* the small motor is fixed-strength/high-frequency. Active effects are renewed
|
||||
* every VBlank with a short lifetime so a crash or unplug cannot leave a host
|
||||
* controller vibrating indefinitely. */
|
||||
static void update_controller_rumble(void) {
|
||||
static const bool trace = [] {
|
||||
const char* e = std::getenv("PSX_RUMBLE_TRACE");
|
||||
return e && e[0] && e[0] != '0';
|
||||
}();
|
||||
for (int s = 0; s < 2; s++) {
|
||||
PlayerInput& p = g_players[s];
|
||||
uint8_t small = 0, large = 0;
|
||||
sio_get_pad_rumble(s, &small, &large);
|
||||
if (!p.handle) {
|
||||
p.rumble_small = small;
|
||||
p.rumble_large = large;
|
||||
p.rumble_known = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool changed = !p.rumble_known || p.rumble_small != small ||
|
||||
p.rumble_large != large;
|
||||
if (small || large || (changed && p.rumble_known)) {
|
||||
const Uint16 low_frequency = (Uint16)((unsigned)large * 257u);
|
||||
const Uint16 high_frequency = small ? 0xFFFFu : 0u;
|
||||
#if defined(PSX_SDL3)
|
||||
const int rc = SDL_GameControllerRumble(
|
||||
p.handle, low_frequency, high_frequency,
|
||||
(small || large) ? 100u : 0u) ? 0 : -1;
|
||||
#else
|
||||
const int rc = SDL_GameControllerRumble(
|
||||
p.handle, low_frequency, high_frequency,
|
||||
(small || large) ? 100u : 0u);
|
||||
#endif
|
||||
if (rc != 0 && !p.rumble_warned) {
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp runtime: controller for slot %d rejected rumble: %s\n",
|
||||
s + 1, SDL_GetError());
|
||||
p.rumble_warned = true;
|
||||
}
|
||||
}
|
||||
if (trace && changed) {
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp rumble: slot=%d small=%u large=%u\n",
|
||||
s + 1, (unsigned)small, (unsigned)large);
|
||||
}
|
||||
p.rumble_small = small;
|
||||
p.rumble_large = large;
|
||||
p.rumble_known = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5043,6 +5109,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
psx_selfcheck_finish_frame(
|
||||
(g_offline_pad_count >= 3 && !sio_get_multitap()) ? 1 : 0);
|
||||
}
|
||||
if (!g_headless) update_controller_rumble();
|
||||
|
||||
/* Latency ring: open this present cycle's slot, stamping when input was
|
||||
* sampled into SIO. Always-on; queried via the debug server "latency". */
|
||||
@@ -10527,6 +10594,10 @@ session_reboot:
|
||||
* as a game controller rather than a raw HID device. */
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI, "1");
|
||||
SDL_SetHint(SDL_HINT_JOYSTICK_RAWINPUT, "0");
|
||||
/* SDL3 aliases the SDL2-era PS5 rumble hint to enhanced reports. Enabling
|
||||
* it also preserves DualSense rumble on the explicit SDL2 fallback. */
|
||||
SDL_SetHintWithPriority(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1",
|
||||
SDL_HINT_OVERRIDE);
|
||||
/* ...but HIDAPI's Xbox sub-driver is OFF by default on Windows (Xbox pads are
|
||||
* normally RAWINPUT/XInput there). With RAWINPUT disabled above, a PHYSICAL
|
||||
* Xbox One/Series controller would be claimed by nobody -> not a GameController
|
||||
|
||||
@@ -1003,6 +1003,28 @@ bool conditions_match(const ModPackage& package, const ModSelection& selection,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool feature_predicate_matches(
|
||||
const ModOverlay::FeaturePredicate& predicate,
|
||||
const std::map<std::string, const ModPackage*>& active_packages,
|
||||
const std::map<std::string, ModSelection>& selections) {
|
||||
if (!predicate.present) return true;
|
||||
bool enabled = false;
|
||||
const auto package_it = active_packages.find(predicate.package_id);
|
||||
if (package_it != active_packages.end()) {
|
||||
const ModPackage& package = *package_it->second;
|
||||
const ModFeature* feature =
|
||||
find_feature(package, predicate.feature_id);
|
||||
if (feature) {
|
||||
const auto selection_it = selections.find(predicate.package_id);
|
||||
const ModSelection blank;
|
||||
const ModSelection& selection =
|
||||
selection_it == selections.end() ? blank : selection_it->second;
|
||||
enabled = is_feature_enabled(package, selection, *feature);
|
||||
}
|
||||
}
|
||||
return enabled == predicate.enabled;
|
||||
}
|
||||
|
||||
bool valid_option_value(const ModOption& option, const std::string& value);
|
||||
|
||||
void read_conditions(const toml::value& value, const std::vector<ModOption>& options,
|
||||
@@ -2100,6 +2122,32 @@ bool ModPackageManager::read_manifest(const fs::path& path, ModPackage& out,
|
||||
v, "order", (int64_t)declaration_index);
|
||||
read_conditions(v, out.options, overlay.feature_id,
|
||||
overlay.when, "overlay");
|
||||
if (v.contains("when_feature")) {
|
||||
if (out.format_version < 4)
|
||||
throw std::runtime_error(
|
||||
"overlay when_feature requires format_version 4");
|
||||
const toml::value& predicate =
|
||||
toml::find(v, "when_feature");
|
||||
for (const auto& [key, unused] : predicate.as_table()) {
|
||||
(void)unused;
|
||||
if (key != "package" && key != "feature" &&
|
||||
key != "enabled")
|
||||
throw std::runtime_error(
|
||||
"overlay when_feature has unknown field: " +
|
||||
key);
|
||||
}
|
||||
overlay.when_feature.present = true;
|
||||
overlay.when_feature.package_id =
|
||||
toml::find<std::string>(predicate, "package");
|
||||
overlay.when_feature.feature_id =
|
||||
toml::find<std::string>(predicate, "feature");
|
||||
overlay.when_feature.enabled =
|
||||
toml::find<bool>(predicate, "enabled");
|
||||
if (!valid_id(overlay.when_feature.package_id) ||
|
||||
!valid_id(overlay.when_feature.feature_id))
|
||||
throw std::runtime_error(
|
||||
"overlay when_feature references an invalid id");
|
||||
}
|
||||
out.overlays.push_back(std::move(overlay));
|
||||
++declaration_index;
|
||||
}
|
||||
@@ -2967,50 +3015,6 @@ ModResolution ModPackageManager::resolve(const std::string& game_id,
|
||||
write.feature_id = patch->feature_id;
|
||||
result.writes.push_back(std::move(write));
|
||||
}
|
||||
std::vector<const ModOverlay*> overlays;
|
||||
overlays.reserve(package->overlays.size());
|
||||
for (const ModOverlay& overlay : package->overlays) {
|
||||
const ModFeature* feature =
|
||||
find_feature(*package, overlay.feature_id);
|
||||
if (!feature ||
|
||||
!is_feature_enabled(*package, selected, *feature) ||
|
||||
!conditions_match(*package, selected,
|
||||
overlay.feature_id, overlay.when))
|
||||
continue;
|
||||
overlays.push_back(&overlay);
|
||||
}
|
||||
std::stable_sort(overlays.begin(), overlays.end(),
|
||||
[](const ModOverlay* a, const ModOverlay* b) {
|
||||
return a->order < b->order;
|
||||
});
|
||||
for (const ModOverlay* overlay : overlays) {
|
||||
std::vector<uint8_t> payload;
|
||||
std::string payload_error;
|
||||
if (!read_file(overlay->file, payload, &payload_error)) {
|
||||
result.errors.push_back(
|
||||
package->id + "/" + overlay->feature_id + ": " +
|
||||
payload_error);
|
||||
continue;
|
||||
}
|
||||
const std::string actual = fingerprint_text(std::string(
|
||||
(const char*)payload.data(), payload.size()));
|
||||
if (payload.size() != overlay->size ||
|
||||
actual != overlay->sha256) {
|
||||
result.errors.push_back(
|
||||
package->id + "/" + overlay->feature_id +
|
||||
": overlay payload changed after installation");
|
||||
continue;
|
||||
}
|
||||
ModResolution::Overlay resolved;
|
||||
resolved.target = overlay->target;
|
||||
resolved.location = overlay->location;
|
||||
resolved.payload = std::move(payload);
|
||||
resolved.payload_sha256 = overlay->sha256;
|
||||
resolved.expected_sha256 = overlay->expected_sha256;
|
||||
resolved.package_id = package->id;
|
||||
resolved.feature_id = overlay->feature_id;
|
||||
result.overlays.push_back(std::move(resolved));
|
||||
}
|
||||
} else {
|
||||
const std::string resolver_id = package->resolver.substr(8);
|
||||
const auto resolver = builtin_resolvers().find(resolver_id);
|
||||
@@ -3021,6 +3025,52 @@ ModResolution ModPackageManager::resolve(const std::string& game_id,
|
||||
result.errors.empty())
|
||||
result.errors.push_back(package->id + ": built-in resolver failed");
|
||||
}
|
||||
std::vector<const ModOverlay*> overlays;
|
||||
overlays.reserve(package->overlays.size());
|
||||
for (const ModOverlay& overlay : package->overlays) {
|
||||
const ModFeature* feature =
|
||||
find_feature(*package, overlay.feature_id);
|
||||
if (!feature ||
|
||||
!is_feature_enabled(*package, selected, *feature) ||
|
||||
!conditions_match(*package, selected,
|
||||
overlay.feature_id, overlay.when) ||
|
||||
!feature_predicate_matches(
|
||||
overlay.when_feature, active, selections_))
|
||||
continue;
|
||||
overlays.push_back(&overlay);
|
||||
}
|
||||
std::stable_sort(overlays.begin(), overlays.end(),
|
||||
[](const ModOverlay* a, const ModOverlay* b) {
|
||||
return a->order < b->order;
|
||||
});
|
||||
for (const ModOverlay* overlay : overlays) {
|
||||
std::vector<uint8_t> payload;
|
||||
std::string payload_error;
|
||||
if (!read_file(overlay->file, payload, &payload_error)) {
|
||||
result.errors.push_back(
|
||||
package->id + "/" + overlay->feature_id + ": " +
|
||||
payload_error);
|
||||
continue;
|
||||
}
|
||||
const std::string actual = fingerprint_text(std::string(
|
||||
(const char*)payload.data(), payload.size()));
|
||||
if (payload.size() != overlay->size ||
|
||||
actual != overlay->sha256) {
|
||||
result.errors.push_back(
|
||||
package->id + "/" + overlay->feature_id +
|
||||
": overlay payload changed after installation");
|
||||
continue;
|
||||
}
|
||||
ModResolution::Overlay resolved;
|
||||
resolved.target = overlay->target;
|
||||
resolved.location = overlay->location;
|
||||
resolved.payload = std::move(payload);
|
||||
resolved.payload_sha256 = overlay->sha256;
|
||||
resolved.expected_sha256 = overlay->expected_sha256;
|
||||
resolved.package_id = package->id;
|
||||
resolved.feature_id = overlay->feature_id;
|
||||
result.overlays.push_back(std::move(resolved));
|
||||
}
|
||||
std::vector<const ModPlugin*> plugins;
|
||||
plugins.reserve(package->plugins.size());
|
||||
for (const ModPlugin& plugin : package->plugins) {
|
||||
|
||||
+90
-20
@@ -153,6 +153,66 @@ void set_error(const std::string& error) {
|
||||
state().error = error;
|
||||
}
|
||||
|
||||
void apply_main_write(const ModResolution::Write& write) {
|
||||
if (write.fields.empty()) {
|
||||
for (size_t i = 0; i < write.replacement.size(); ++i)
|
||||
psx_write_byte((uint32_t)write.location + (uint32_t)i,
|
||||
write.replacement[i]);
|
||||
dirty_ram_mark_executable_range(
|
||||
(uint32_t)write.location & 0x1FFFFFFFu,
|
||||
(uint32_t)write.replacement.size());
|
||||
return;
|
||||
}
|
||||
for (const ModResolution::Write::Field& field : write.fields) {
|
||||
for (size_t i = 0; i < field.replacement.size(); ++i)
|
||||
psx_write_byte(
|
||||
(uint32_t)write.location +
|
||||
(uint32_t)field.offset + (uint32_t)i,
|
||||
field.replacement[i]);
|
||||
dirty_ram_mark_executable_range(
|
||||
((uint32_t)write.location +
|
||||
(uint32_t)field.offset) & 0x1FFFFFFFu,
|
||||
(uint32_t)field.replacement.size());
|
||||
}
|
||||
}
|
||||
|
||||
bool restored_main_matches_plan(const RuntimeMods& s, uint32_t& failed_at) {
|
||||
std::map<uint32_t, uint8_t> desired;
|
||||
for (const ModResolution::Write& write : s.plan.writes) {
|
||||
if (write.target != ModPatchTarget::MainExe) continue;
|
||||
if (write.fields.empty()) {
|
||||
for (size_t i = 0; i < write.replacement.size(); ++i)
|
||||
desired[(uint32_t)write.location + (uint32_t)i] =
|
||||
write.replacement[i];
|
||||
} else {
|
||||
for (const ModResolution::Write::Field& field : write.fields) {
|
||||
for (size_t i = 0; i < field.replacement.size(); ++i)
|
||||
desired[
|
||||
(uint32_t)write.location +
|
||||
(uint32_t)field.offset + (uint32_t)i] =
|
||||
field.replacement[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const ModResolution::Write& write : s.plan.writes) {
|
||||
if (write.target != ModPatchTarget::MainExe) continue;
|
||||
for (size_t i = 0; i < write.expected.size(); ++i) {
|
||||
const uint32_t address =
|
||||
(uint32_t)write.location + (uint32_t)i;
|
||||
const uint8_t observed = psx_read_byte(address);
|
||||
if (observed == write.expected[i]) continue;
|
||||
const auto replacement = desired.find(address);
|
||||
if (replacement != desired.end() &&
|
||||
observed == replacement->second)
|
||||
continue;
|
||||
failed_at = address;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool sha256_file(const std::filesystem::path& path, std::string& out,
|
||||
std::string* error) {
|
||||
out.clear();
|
||||
@@ -1102,26 +1162,7 @@ extern "C" void mod_runtime_on_dispatch(uint32_t target) {
|
||||
}
|
||||
for (const ModResolution::Write& write : s.plan.writes) {
|
||||
if (write.target != ModPatchTarget::MainExe) continue;
|
||||
if (write.fields.empty()) {
|
||||
for (size_t i = 0; i < write.replacement.size(); ++i)
|
||||
psx_write_byte((uint32_t)write.location + (uint32_t)i,
|
||||
write.replacement[i]);
|
||||
dirty_ram_mark_executable_range(
|
||||
(uint32_t)write.location & 0x1FFFFFFFu,
|
||||
(uint32_t)write.replacement.size());
|
||||
} else {
|
||||
for (const ModResolution::Write::Field& field : write.fields) {
|
||||
for (size_t i = 0; i < field.replacement.size(); ++i)
|
||||
psx_write_byte(
|
||||
(uint32_t)write.location +
|
||||
(uint32_t)field.offset + (uint32_t)i,
|
||||
field.replacement[i]);
|
||||
dirty_ram_mark_executable_range(
|
||||
((uint32_t)write.location +
|
||||
(uint32_t)field.offset) & 0x1FFFFFFFu,
|
||||
(uint32_t)field.replacement.size());
|
||||
}
|
||||
}
|
||||
apply_main_write(write);
|
||||
}
|
||||
s.main_applied = true;
|
||||
if (!s.plan.writes.empty())
|
||||
@@ -1129,6 +1170,35 @@ extern "C" void mod_runtime_on_dispatch(uint32_t target) {
|
||||
s.plan.fingerprint.c_str());
|
||||
}
|
||||
|
||||
extern "C" void mod_runtime_on_savestate_loaded(void) {
|
||||
using namespace PSXRecompV4;
|
||||
RuntimeMods& s = state();
|
||||
if (!s.initialized || !s.plan.ok) return;
|
||||
|
||||
if (!s.main_applied) {
|
||||
uint32_t failed_at = 0;
|
||||
if (!restored_main_matches_plan(s, failed_at)) {
|
||||
std::fprintf(stderr,
|
||||
"psxrecomp: mod plan %s rejected after savestate restore at "
|
||||
"0x%08X (expected-byte guard failed)\n",
|
||||
s.plan.fingerprint.c_str(), (unsigned)failed_at);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool applied = false;
|
||||
for (const ModResolution::Write& write : s.plan.writes) {
|
||||
if (write.target != ModPatchTarget::MainExe) continue;
|
||||
apply_main_write(write);
|
||||
applied = true;
|
||||
}
|
||||
s.main_applied = true;
|
||||
if (applied)
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp: reapplied mod plan %s after savestate restore\n",
|
||||
s.plan.fingerprint.c_str());
|
||||
}
|
||||
|
||||
extern "C" void mod_runtime_enable_disc_patches(void) {
|
||||
PSXRecompV4::state().disc_enabled = true;
|
||||
}
|
||||
|
||||
+101
-6
@@ -53,6 +53,16 @@ static uint8_t pad_stick[PSX_MAX_PLAYERS][4] = {
|
||||
[0 ... PSX_MAX_PLAYERS - 1] = { 0x80, 0x80, 0x80, 0x80 }
|
||||
}; /* lx,ly,rx,ry */
|
||||
|
||||
/* DualShock command 0x4D maps the six writable bytes in a 0x42 poll onto the
|
||||
* two motors: 0x00 = small/high-frequency, 0x01 = large/low-frequency,
|
||||
* 0xFF = unused. The map powers up unassigned and is echoed back while a new
|
||||
* map is latched, matching the physical pad/Mednafen protocol. */
|
||||
static uint8_t pad_rumble_map[PSX_MAX_PLAYERS][6] = {
|
||||
[0 ... PSX_MAX_PLAYERS - 1] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF },
|
||||
};
|
||||
static uint8_t pad_rumble_small[PSX_MAX_PLAYERS];
|
||||
static uint8_t pad_rumble_large[PSX_MAX_PLAYERS];
|
||||
|
||||
/* Analog-mode lock, per logical pad. A real DualShock's config command 0x44
|
||||
* 0x..02/0x03 locks/unlocks the mode (dualshock.cpp:714-725); a locked pad
|
||||
* ignores the physical analog button (dualshock.cpp:203). We emulate the
|
||||
@@ -691,6 +701,9 @@ void sio_init(void) {
|
||||
pad_response_idx = 0;
|
||||
pad_current_cmd = 0;
|
||||
pad_active_logical = 0;
|
||||
memset(pad_rumble_map, 0xFF, sizeof(pad_rumble_map));
|
||||
memset(pad_rumble_small, 0, sizeof(pad_rumble_small));
|
||||
memset(pad_rumble_large, 0, sizeof(pad_rumble_large));
|
||||
for (int i = 0; i < PSX_MAX_PLAYERS; i++) {
|
||||
pad_buttons[i] = 0xFFFF;
|
||||
pad_analog[i] = 0;
|
||||
@@ -830,8 +843,13 @@ void sio_connect_pad(int slot) {
|
||||
|
||||
void sio_set_pad_connected(int slot, int connected) {
|
||||
if (slot < 0 || slot >= PSX_MAX_PLAYERS) return;
|
||||
if (connected) pad_connected |= (uint8_t)(1u << slot);
|
||||
else pad_connected &= (uint8_t)~(1u << slot);
|
||||
if (connected) {
|
||||
pad_connected |= (uint8_t)(1u << slot);
|
||||
} else {
|
||||
pad_connected &= (uint8_t)~(1u << slot);
|
||||
pad_rumble_small[slot] = 0;
|
||||
pad_rumble_large[slot] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void sio_set_pad_config_capable(int slot, int capable) {
|
||||
@@ -840,7 +858,21 @@ void sio_set_pad_config_capable(int slot, int capable) {
|
||||
pad_supports_config[slot] = capable ? 1 : 0;
|
||||
/* A plain digital pad can never be in config mode; clear any stale latch so
|
||||
* the next poll reports the digital id (0x41), not the config id (0xF3). */
|
||||
if (!capable) pad_in_config[slot] = 0;
|
||||
if (!capable) {
|
||||
pad_in_config[slot] = 0;
|
||||
pad_rumble_small[slot] = 0;
|
||||
pad_rumble_large[slot] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void sio_get_pad_rumble(int slot, uint8_t *small, uint8_t *large) {
|
||||
uint8_t s = 0, l = 0;
|
||||
if (slot >= 0 && slot < PSX_MAX_PLAYERS && (pad_connected & (1u << slot))) {
|
||||
s = pad_rumble_small[slot];
|
||||
l = pad_rumble_large[slot];
|
||||
}
|
||||
if (small) *small = s;
|
||||
if (large) *large = l;
|
||||
}
|
||||
|
||||
void sio_set_pad_state(uint16_t buttons) {
|
||||
@@ -1196,6 +1228,10 @@ static void pad_process_byte(uint8_t tx_byte) {
|
||||
* (axis5_sio_controller.md D8). */
|
||||
if (tx_byte == 0x45)
|
||||
pad_response[3] = pad_analog[lp] ? 0x01 : 0x00;
|
||||
/* 0x4D returns the previous six-byte motor map while latching the
|
||||
* replacement bytes later in this same transaction. */
|
||||
if (tx_byte == 0x4D)
|
||||
memcpy(&pad_response[2], pad_rumble_map[lp], 6);
|
||||
pad_response_len = 8;
|
||||
pad_state = PAD_SEND_RESPONSE;
|
||||
sio_rx_data = pad_response[0];
|
||||
@@ -1219,6 +1255,33 @@ static void pad_process_byte(uint8_t tx_byte) {
|
||||
pad_mtap_addr == 0x01 && pad_response_idx == 1 &&
|
||||
selected_slot >= 0 && selected_slot <= 1)
|
||||
mtap_req_this[selected_slot] = (tx_byte == 0x01) ? 1 : 0;
|
||||
/* DualShock rumble: index by the active logical pad (multitap-aware). */
|
||||
{
|
||||
const int rs = (pad_active_logical >= 0 && pad_active_logical < PSX_MAX_PLAYERS)
|
||||
? pad_active_logical
|
||||
: selected_slot;
|
||||
if (rs >= 0 && rs < PSX_MAX_PLAYERS) {
|
||||
/* The six data bytes after 0x42's leading 0x00 occupy response
|
||||
* indexes 2..7. Route each through the map negotiated by 0x4D. */
|
||||
if (pad_current_cmd == 0x42 &&
|
||||
pad_response_idx >= 2 && pad_response_idx < 8) {
|
||||
const unsigned map_index = (unsigned)pad_response_idx - 2u;
|
||||
const uint8_t motor = pad_rumble_map[rs][map_index];
|
||||
if (motor == 0x00)
|
||||
pad_rumble_small[rs] = tx_byte;
|
||||
else if (motor == 0x01)
|
||||
pad_rumble_large[rs] = tx_byte;
|
||||
}
|
||||
/* 0x4D uses the same six wire positions. pad_response[] was
|
||||
* populated with the old values before the transaction, so
|
||||
* updating the live map here preserves echo-before-write. */
|
||||
if (pad_current_cmd == 0x4D &&
|
||||
pad_response_idx >= 2 && pad_response_idx < 8) {
|
||||
const unsigned map_index = (unsigned)pad_response_idx - 2u;
|
||||
pad_rumble_map[rs][map_index] = tx_byte;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* For 0x43 (enter/exit config), the data byte selecting enter(0x01)/
|
||||
* exit(0x00) arrives paired with response index 2. Latch the new config
|
||||
* state; it takes effect from the next transaction (the ID byte already
|
||||
@@ -2546,18 +2609,26 @@ static int sio_snap_emit_fsm_meta(PstW *w) {
|
||||
pst_w_u32(w, sio_irq_pending_byte_seq);
|
||||
}
|
||||
|
||||
/* DualShock rumble map/motors — appended after meta so pre-rumble snapshots
|
||||
* remain loadable and netplay digests that stop at fsm_pace stay stable. */
|
||||
static int sio_snap_emit_rumble(PstW *w) {
|
||||
return pst_w_bytes(w, pad_rumble_map, sizeof(pad_rumble_map)) &&
|
||||
pst_w_bytes(w, pad_rumble_small, sizeof(pad_rumble_small)) &&
|
||||
pst_w_bytes(w, pad_rumble_large, sizeof(pad_rumble_large));
|
||||
}
|
||||
|
||||
static int sio_snap_emit_fsm(PstW *w) {
|
||||
return sio_snap_emit_fsm_pace(w) && sio_snap_emit_fsm_meta(w);
|
||||
}
|
||||
|
||||
static int sio_snap_emit(PstW *w) {
|
||||
return sio_snap_emit_regs(w) && sio_snap_emit_pads(w) &&
|
||||
sio_snap_emit_mc(w) && sio_snap_emit_fsm(w);
|
||||
sio_snap_emit_mc(w) && sio_snap_emit_fsm(w) && sio_snap_emit_rumble(w);
|
||||
}
|
||||
|
||||
/* Cumulative section end offsets in the snapshot wire:
|
||||
* out[0]=regs, out[1]=pads, out[2]=memcard, out[3]=fsm_pace (netplay),
|
||||
* out[4]=full including fsm_meta (== sio_snapshot_bytes()). */
|
||||
* out[4]=full including fsm_meta + rumble (== sio_snapshot_bytes()). */
|
||||
void sio_snapshot_section_ends(uint32_t out[5]) {
|
||||
PstW w;
|
||||
pst_w_init(&w, NULL, 0);
|
||||
@@ -2570,6 +2641,7 @@ void sio_snapshot_section_ends(uint32_t out[5]) {
|
||||
(void)sio_snap_emit_fsm_pace(&w);
|
||||
out[3] = (uint32_t)w.written;
|
||||
(void)sio_snap_emit_fsm_meta(&w);
|
||||
(void)sio_snap_emit_rumble(&w);
|
||||
out[4] = (uint32_t)w.written;
|
||||
}
|
||||
|
||||
@@ -2652,6 +2724,24 @@ static int sio_snap_parse(PstR *r) {
|
||||
* restored byte_seq so the next TX does not stamp a peer-divergent
|
||||
* seq into sio_irq_pending_byte_seq (was forking fsm digests alone). */
|
||||
sio_trace_seq = sio_irq_pending_byte_seq;
|
||||
/* Pre-rumble snapshots end here. A mid-game legacy state cannot tell us
|
||||
* the negotiated map, so assume the standard 0x00/0x01 layout used by
|
||||
* commercial DualShock games while leaving both motors stopped. */
|
||||
if (r->p == r->end) {
|
||||
memset(pad_rumble_map, 0xFF, sizeof(pad_rumble_map));
|
||||
memset(pad_rumble_small, 0, sizeof(pad_rumble_small));
|
||||
memset(pad_rumble_large, 0, sizeof(pad_rumble_large));
|
||||
for (int s = 0; s < PSX_MAX_PLAYERS; s++) {
|
||||
pad_rumble_map[s][0] = 0x00;
|
||||
pad_rumble_map[s][1] = 0x01;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
if (!pst_r_bytes(r, pad_rumble_map, sizeof(pad_rumble_map)) ||
|
||||
!pst_r_bytes(r, pad_rumble_small, sizeof(pad_rumble_small)) ||
|
||||
!pst_r_bytes(r, pad_rumble_large, sizeof(pad_rumble_large)))
|
||||
return 0;
|
||||
if (r->p != r->end) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -2671,7 +2761,12 @@ void sio_snapshot_write(uint8_t *p) {
|
||||
|
||||
int sio_snapshot_read(const uint8_t *p, uint32_t len) {
|
||||
PstR r;
|
||||
if (len != sio_snapshot_bytes()) return 0;
|
||||
const uint32_t current = sio_snapshot_bytes();
|
||||
const uint32_t rumble_bytes = (uint32_t)(sizeof(pad_rumble_map) +
|
||||
sizeof(pad_rumble_small) +
|
||||
sizeof(pad_rumble_large));
|
||||
if (len != current && (len > current || len + rumble_bytes != current))
|
||||
return 0;
|
||||
pst_r_init(&r, p, len);
|
||||
return sio_snap_parse(&r);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,39 @@ extern "C" void psx_write_byte(uint32_t address, uint8_t value) {
|
||||
ram[address & 0x1fffffu] = value;
|
||||
}
|
||||
|
||||
extern "C" uint16_t psx_read_half(uint32_t address) {
|
||||
const uint32_t offset = address & 0x1fffffu;
|
||||
return (uint16_t)(ram[offset] | ((uint16_t)ram[offset + 1] << 8));
|
||||
}
|
||||
|
||||
extern "C" void psx_write_half(uint32_t address, uint16_t value) {
|
||||
const uint32_t offset = address & 0x1fffffu;
|
||||
ram[offset] = (uint8_t)value;
|
||||
ram[offset + 1] = (uint8_t)(value >> 8);
|
||||
}
|
||||
|
||||
extern "C" uint32_t psx_read_word(uint32_t address) {
|
||||
const uint32_t offset = address & 0x1fffffu;
|
||||
return (uint32_t)ram[offset] |
|
||||
((uint32_t)ram[offset + 1] << 8) |
|
||||
((uint32_t)ram[offset + 2] << 16) |
|
||||
((uint32_t)ram[offset + 3] << 24);
|
||||
}
|
||||
|
||||
extern "C" void psx_write_word(uint32_t address, uint32_t value) {
|
||||
const uint32_t offset = address & 0x1fffffu;
|
||||
ram[offset] = (uint8_t)value;
|
||||
ram[offset + 1] = (uint8_t)(value >> 8);
|
||||
ram[offset + 2] = (uint8_t)(value >> 16);
|
||||
ram[offset + 3] = (uint8_t)(value >> 24);
|
||||
}
|
||||
|
||||
extern "C" uint32_t psx_mod_memory_alloc(uint32_t, uint32_t) { return 0; }
|
||||
extern "C" uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t, uint32_t) {
|
||||
return 0;
|
||||
}
|
||||
extern "C" int psx_ws_x_margin(void) { return 0; }
|
||||
|
||||
extern "C" void dirty_ram_mark_executable_range(uint32_t, uint32_t) {}
|
||||
extern "C" int fntrace_is_game_started(void) { return 1; }
|
||||
|
||||
@@ -292,6 +325,21 @@ int main() {
|
||||
"adjacent sparse fields must compose while preserving guard-only "
|
||||
"bytes");
|
||||
|
||||
/* A full-machine savestate replaces main RAM after the entry-point plan
|
||||
* has already applied. Loading a stock checkpoint must not silently turn
|
||||
* the currently enabled main-EXE features back off. */
|
||||
ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4;
|
||||
ram[0x1100] = 0; ram[0x1101] = 0;
|
||||
ram[0x1102] = 1; ram[0x1103] = 0;
|
||||
ram[0x1200] = 2; ram[0x1201] = 0;
|
||||
ram[0x1202] = 1; ram[0x1203] = 0x32;
|
||||
mod_runtime_on_savestate_loaded();
|
||||
check(ram[0x1000] == 0xa1 && ram[0x1003] == 0xa4 &&
|
||||
ram[0x1100] == 42 && ram[0x1102] == 43 &&
|
||||
ram[0x1200] == 1 && ram[0x1201] == 0x42 &&
|
||||
ram[0x1202] == 0 && ram[0x1203] == 0x32,
|
||||
"savestate restore must reapply the complete enabled main plan");
|
||||
|
||||
std::array<uint8_t, 2352> sector{};
|
||||
sector[10] = 0xaa;
|
||||
mod_runtime_patch_disc_sector(2, 1, sector.data(), (uint32_t)sector.size());
|
||||
@@ -374,6 +422,26 @@ int main() {
|
||||
"a failed sparse guard-only byte must leave the complete main plan "
|
||||
"untouched");
|
||||
|
||||
/* Loading can be requested while the boot executable is still running,
|
||||
* before the configured game entry point has dispatched. The restored
|
||||
* checkpoint itself supplies the bytes used to validate and apply the
|
||||
* plan in that case. */
|
||||
check(PSXRecompV4::mod_runtime_initialize(
|
||||
root, "SLUS-RUNTIME", 0x80002000, {}, &error),
|
||||
error.c_str());
|
||||
check(PSXRecompV4::mod_runtime_commit(stock_path, &error), error.c_str());
|
||||
ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4;
|
||||
ram[0x1100] = 0; ram[0x1101] = 0;
|
||||
ram[0x1102] = 1; ram[0x1103] = 0;
|
||||
ram[0x1200] = 2; ram[0x1201] = 0;
|
||||
ram[0x1202] = 1; ram[0x1203] = 0x32;
|
||||
mod_runtime_on_savestate_loaded();
|
||||
check(ram[0x1000] == 0xa1 && ram[0x1003] == 0xa4 &&
|
||||
ram[0x1100] == 42 && ram[0x1102] == 43 &&
|
||||
ram[0x1200] == 1 && ram[0x1201] == 0x42 &&
|
||||
ram[0x1202] == 0 && ram[0x1203] == 0x32,
|
||||
"pre-entry savestate restore must validate and apply the main plan");
|
||||
|
||||
mod_runtime_enable_disc_patches();
|
||||
std::array<uint8_t, 2352> bad_sparse_disc{};
|
||||
bad_sparse_disc[15] = 2;
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
/* DualShock rumble protocol regression: 0x4D map negotiation must feed the
|
||||
* small/large motor bytes from later 0x42 polls to the frontend-facing state. */
|
||||
|
||||
#include "sio.h"
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
uint32_t sio_snapshot_bytes(void);
|
||||
void sio_snapshot_write(uint8_t *p);
|
||||
int sio_snapshot_read(const uint8_t *p, uint32_t len);
|
||||
|
||||
uint32_t i_stat = 0;
|
||||
uint32_t g_debug_current_func_addr = 0;
|
||||
int psx_get_in_exception(void) { return 0; }
|
||||
uint8_t psx_read_byte(uint32_t addr) { (void)addr; return 0; }
|
||||
uint32_t memory_get_sr(void) { return 0; }
|
||||
void debug_server_poll(void) {}
|
||||
void debug_server_log_sio_write(uint32_t a, uint32_t v, uint8_t w) {
|
||||
(void)a; (void)v; (void)w;
|
||||
}
|
||||
void starvation_ring_record(uint8_t k, uint8_t tx, uint8_t rx, uint16_t c,
|
||||
uint16_t st, uint8_t d, uint8_t ss, uint16_t ms,
|
||||
uint8_t mc, uint16_t sec, uint8_t di, uint32_t f) {
|
||||
(void)k; (void)tx; (void)rx; (void)c; (void)st; (void)d; (void)ss;
|
||||
(void)ms; (void)mc; (void)sec; (void)di; (void)f;
|
||||
}
|
||||
void card_read_summary_record(uint8_t s, uint8_t c, uint16_t sec, uint8_t e,
|
||||
uint8_t chk, uint8_t d0, uint8_t d1, uint32_t f) {
|
||||
(void)s; (void)c; (void)sec; (void)e; (void)chk; (void)d0; (void)d1; (void)f;
|
||||
}
|
||||
void card_data_writes_arm(uint8_t v, uint16_t s, uint8_t i, uint8_t sl) {
|
||||
(void)v; (void)s; (void)i; (void)sl;
|
||||
}
|
||||
void event_ring_record_aux(uint16_t k, uint8_t d, uint32_t a) {
|
||||
(void)k; (void)d; (void)a;
|
||||
}
|
||||
void psx_irq_raise(uint32_t bit, uint32_t detail) {
|
||||
(void)detail; i_stat |= 1u << bit;
|
||||
}
|
||||
int memcard_is_present(int card) { (void)card; return 0; }
|
||||
int memcard_read_sector(int card, int sector, uint8_t *buf) {
|
||||
(void)card; (void)sector; memset(buf, 0, 128); return -1;
|
||||
}
|
||||
int memcard_write_sector(int card, int sector, const uint8_t *buf) {
|
||||
(void)card; (void)sector; (void)buf; return -1;
|
||||
}
|
||||
void memcard_flush(int card) { (void)card; }
|
||||
|
||||
#define SIO_TX_DATA 0x1F801040u
|
||||
#define SIO_RX_DATA 0x1F801040u
|
||||
#define SIO_CTRL 0x1F80104Au
|
||||
#define CTRL_TX_EN (1u << 0)
|
||||
#define CTRL_SELECT (1u << 1)
|
||||
#define CTRL_ACK (1u << 4)
|
||||
#define CTRL_ACK_IRQ_EN (1u << 12)
|
||||
#define CTRL_SLOT (1u << 13)
|
||||
|
||||
static int failures;
|
||||
|
||||
#define EXPECT(label, expected, actual) do { \
|
||||
unsigned e_ = (unsigned)(expected), a_ = (unsigned)(actual); \
|
||||
if (e_ != a_) { \
|
||||
fprintf(stderr, "FAIL %s: expected=0x%X actual=0x%X\n", \
|
||||
label, e_, a_); \
|
||||
failures++; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static uint8_t xchg(int slot, uint8_t tx) {
|
||||
const uint16_t ctrl = (uint16_t)(CTRL_TX_EN | CTRL_SELECT |
|
||||
CTRL_ACK_IRQ_EN | (slot ? CTRL_SLOT : 0));
|
||||
sio_write(SIO_CTRL, ctrl);
|
||||
sio_write(SIO_TX_DATA, tx);
|
||||
sio_tick(2000);
|
||||
const uint8_t rx = (uint8_t)sio_read(SIO_RX_DATA);
|
||||
sio_write(SIO_CTRL, ctrl | CTRL_ACK);
|
||||
return rx;
|
||||
}
|
||||
|
||||
static void finish_six_data_bytes(int slot, unsigned already_sent) {
|
||||
while (already_sent++ < 6) (void)xchg(slot, 0x00);
|
||||
}
|
||||
|
||||
static void enter_config(int slot) {
|
||||
EXPECT("enter.prefix", 0xFF, xchg(slot, 0x01));
|
||||
EXPECT("enter.id", 0x73, xchg(slot, 0x43));
|
||||
EXPECT("enter.ack", 0x5A, xchg(slot, 0x00));
|
||||
(void)xchg(slot, 0x01);
|
||||
finish_six_data_bytes(slot, 1);
|
||||
}
|
||||
|
||||
static void set_rumble_map(int slot, const uint8_t map[6],
|
||||
const uint8_t expected_old[6]) {
|
||||
EXPECT("map.prefix", 0xFF, xchg(slot, 0x01));
|
||||
EXPECT("map.id", 0xF3, xchg(slot, 0x4D));
|
||||
EXPECT("map.ack", 0x5A, xchg(slot, 0x00));
|
||||
for (int i = 0; i < 6; i++)
|
||||
EXPECT("map.echo", expected_old[i], xchg(slot, map[i]));
|
||||
}
|
||||
|
||||
static void exit_config(int slot) {
|
||||
EXPECT("exit.prefix", 0xFF, xchg(slot, 0x01));
|
||||
EXPECT("exit.id", 0xF3, xchg(slot, 0x43));
|
||||
EXPECT("exit.ack", 0x5A, xchg(slot, 0x00));
|
||||
(void)xchg(slot, 0x00);
|
||||
finish_six_data_bytes(slot, 1);
|
||||
}
|
||||
|
||||
static void poll_with_motors(int slot, uint8_t small, uint8_t large) {
|
||||
EXPECT("poll.prefix", 0xFF, xchg(slot, 0x01));
|
||||
EXPECT("poll.id", 0x73, xchg(slot, 0x42));
|
||||
EXPECT("poll.ack", 0x5A, xchg(slot, 0x00));
|
||||
(void)xchg(slot, small);
|
||||
(void)xchg(slot, large);
|
||||
finish_six_data_bytes(slot, 2);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
static const uint8_t unassigned[6] = {
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
|
||||
};
|
||||
static const uint8_t standard[6] = {
|
||||
0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF
|
||||
};
|
||||
uint8_t small = 0, large = 0;
|
||||
|
||||
sio_init();
|
||||
sio_connect_pad(0);
|
||||
sio_set_pad_analog(0, 1, 0x80, 0x80, 0x80, 0x80);
|
||||
sio_set_pad_config_capable(0, 1);
|
||||
|
||||
enter_config(0);
|
||||
set_rumble_map(0, standard, unassigned);
|
||||
set_rumble_map(0, standard, standard);
|
||||
exit_config(0);
|
||||
|
||||
poll_with_motors(0, 0x01, 0x80);
|
||||
sio_get_pad_rumble(0, &small, &large);
|
||||
EXPECT("rumble.small.on", 0x01, small);
|
||||
EXPECT("rumble.large.strength", 0x80, large);
|
||||
|
||||
const uint32_t snapshot_len = sio_snapshot_bytes();
|
||||
uint8_t *snapshot = (uint8_t *)malloc(snapshot_len);
|
||||
if (!snapshot) {
|
||||
fprintf(stderr, "FAIL snapshot allocation\n");
|
||||
return 1;
|
||||
}
|
||||
sio_snapshot_write(snapshot);
|
||||
|
||||
poll_with_motors(0, 0x00, 0x00);
|
||||
sio_get_pad_rumble(0, &small, &large);
|
||||
EXPECT("rumble.small.off", 0x00, small);
|
||||
EXPECT("rumble.large.off", 0x00, large);
|
||||
|
||||
EXPECT("snapshot.current.read", 1,
|
||||
sio_snapshot_read(snapshot, snapshot_len));
|
||||
sio_get_pad_rumble(0, &small, &large);
|
||||
EXPECT("snapshot.current.small", 0x01, small);
|
||||
EXPECT("snapshot.current.large", 0x80, large);
|
||||
|
||||
/* The final 16 bytes are the new rumble fields. Truncating them models a
|
||||
* v1.0.4 SIO section and must load safely with both motors stopped. */
|
||||
EXPECT("snapshot.legacy.read", 1,
|
||||
sio_snapshot_read(snapshot, snapshot_len - 16));
|
||||
sio_get_pad_rumble(0, &small, &large);
|
||||
EXPECT("snapshot.legacy.small", 0x00, small);
|
||||
EXPECT("snapshot.legacy.large", 0x00, large);
|
||||
free(snapshot);
|
||||
|
||||
poll_with_motors(0, 0x01, 0xFF);
|
||||
sio_set_pad_connected(0, 0);
|
||||
sio_get_pad_rumble(0, &small, &large);
|
||||
EXPECT("rumble.disconnect.small", 0x00, small);
|
||||
EXPECT("rumble.disconnect.large", 0x00, large);
|
||||
|
||||
if (failures) {
|
||||
fprintf(stderr, "DualShock rumble protocol: %d failure(s)\n", failures);
|
||||
return 1;
|
||||
}
|
||||
fprintf(stderr, "DualShock rumble protocol: passed\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user