mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Accelerate exact startup file hashing with native host crypto
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
include_guard(GLOBAL)
|
||||
|
||||
# Host utility only: both guest execution profiles retain exactly the same
|
||||
# file identity. No OpenSSL installation is required for a portable build.
|
||||
function(psxrecomp_sha256_file target)
|
||||
target_sources(${target} PRIVATE
|
||||
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../runtime/src/psx_sha256_file.cpp")
|
||||
target_include_directories(${target} PRIVATE
|
||||
"${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../runtime/include")
|
||||
if(WIN32)
|
||||
target_link_libraries(${target} PRIVATE bcrypt)
|
||||
else()
|
||||
find_package(OpenSSL QUIET COMPONENTS Crypto)
|
||||
if(TARGET OpenSSL::Crypto)
|
||||
target_compile_definitions(${target} PRIVATE PSX_SHA256_FILE_OPENSSL=1)
|
||||
target_link_libraries(${target} PRIVATE OpenSSL::Crypto)
|
||||
endif()
|
||||
endif()
|
||||
endfunction()
|
||||
@@ -157,3 +157,30 @@ It excludes retail disc/BIOS and player saves, retains audited native AOT pairs,
|
||||
and passes execution identity/catalog/ZIP checks. It predates the shader
|
||||
batching comparison and is not final parity acceptance. No Linux candidate or
|
||||
measured V8 load-speed gain is claimed.
|
||||
|
||||
The shader-only V8 comparison did eliminate filter-change splits and reduced
|
||||
CPU flush time, but it did not establish a GPU/FPS improvement. Even the first
|
||||
window's similar primitive counts hid different textured fractions; later
|
||||
windows and camera images diverged. Host precision/mod-renderer state around
|
||||
the saved checkpoint needs investigation. The earlier b0/d8 title candidate
|
||||
remains qualified, with the 7c executable and observations retained separately.
|
||||
Use a fixed-work renderer probe before drawing a performance conclusion.
|
||||
|
||||
Shared regular-file identity hashing now uses Windows BCrypt or available
|
||||
Linux OpenSSL, with the portable SHA-256 implementation as fallback. Failed
|
||||
native initialization, update or finalization restarts the entire file;
|
||||
I/O failure leaves the caller's digest untouched. The existing C SHA context
|
||||
and CHD decoded-sector fingerprint path are unchanged. This host utility
|
||||
preserves exact identity in both execution profiles; it is not a guest HLE
|
||||
selection or a shortcut that skips content verification.
|
||||
|
||||
The Windows native/portable suites, mod runtime, resident-loading and PGXP
|
||||
session CTests pass (5/5). Linux OpenSSL and portable builds pass 68 and 67
|
||||
checks respectively, including Unicode paths, SHA block/read-chunk boundaries,
|
||||
known vectors, partially consumed native failures and unreadable inputs.
|
||||
An isolated interleaved six-sample Windows measurement on V8's 257,200,608-byte
|
||||
data track matches Python's SHA-256 in every sample: portable median 0.656533 s,
|
||||
native median 0.130985 s (5.012x for hashing). Receipts are private under
|
||||
`_build-parity-render-perf-20261006/{validate_hash,linux-hash-validation,benchmark_hash}.json`.
|
||||
This saves about half a second for that operation, not the entire multi-second
|
||||
launch delay observed on the busy host. Overall startup still needs attribution.
|
||||
|
||||
+22
-2
@@ -23,6 +23,24 @@ psxrecomp_add_runtime_target(psx-runtime
|
||||
# (docs/ENHANCEMENTS.md G1.10). Titles get their <exe>_pgxp the same way.
|
||||
|
||||
if(BUILD_TESTING)
|
||||
# White-box failure injection checks restart after partial native hashing.
|
||||
foreach(_hash_variant native portable)
|
||||
add_executable(sha256_file_${_hash_variant}_test
|
||||
tests/test_psx_sha256_file.cpp src/psx_sha256.c)
|
||||
target_include_directories(sha256_file_${_hash_variant}_test PRIVATE include)
|
||||
if(_hash_variant STREQUAL "portable")
|
||||
target_compile_definitions(sha256_file_${_hash_variant}_test PRIVATE
|
||||
PSX_SHA256_FILE_PORTABLE=1)
|
||||
elseif(WIN32)
|
||||
target_link_libraries(sha256_file_${_hash_variant}_test PRIVATE bcrypt)
|
||||
elseif(TARGET OpenSSL::Crypto)
|
||||
target_compile_definitions(sha256_file_${_hash_variant}_test PRIVATE
|
||||
PSX_SHA256_FILE_OPENSSL=1)
|
||||
target_link_libraries(sha256_file_${_hash_variant}_test PRIVATE OpenSSL::Crypto)
|
||||
endif()
|
||||
add_test(NAME sha256_file_${_hash_variant}_test
|
||||
COMMAND sha256_file_${_hash_variant}_test)
|
||||
endforeach()
|
||||
add_executable(disasm_test tests/test_disasm.cpp src/disasm_shim.cpp
|
||||
../recompiler/src/mips_decoder.cpp)
|
||||
target_include_directories(disasm_test PRIVATE include ../recompiler/include)
|
||||
@@ -522,6 +540,7 @@ if(BUILD_TESTING)
|
||||
target_compile_definitions(pgxp_session_test PRIVATE
|
||||
PSX_BUILTIN_PGXP_MANIFEST="${CMAKE_CURRENT_SOURCE_DIR}/../mods/builtin/packages/psx.enhancement.pgxp/1.0.0/manifest.toml")
|
||||
target_link_libraries(pgxp_session_test PRIVATE chdr-static)
|
||||
psxrecomp_sha256_file(pgxp_session_test)
|
||||
if(WIN32)
|
||||
if(MSVC)
|
||||
target_link_options(pgxp_session_test PRIVATE /STACK:16777216)
|
||||
@@ -862,8 +881,8 @@ if(BUILD_TESTING)
|
||||
../recompiler/include
|
||||
../recompiler/lib/toml11)
|
||||
target_link_libraries(mod_runtime_test PRIVATE chdr-static)
|
||||
# The real disc hasher uses a 1 MiB scratch buffer. Match the runtime's
|
||||
# stack allowance instead of overflowing MinGW's 1 MiB executable default.
|
||||
psxrecomp_sha256_file(mod_runtime_test)
|
||||
# Match the runtime's stack allowance for the remaining disc/mod helpers.
|
||||
if(WIN32)
|
||||
if(MSVC)
|
||||
target_link_options(mod_runtime_test PRIVATE /STACK:16777216)
|
||||
@@ -889,6 +908,7 @@ if(BUILD_TESTING)
|
||||
../recompiler/include
|
||||
../recompiler/lib/toml11)
|
||||
target_link_libraries(mod_resident_test PRIVATE chdr-static)
|
||||
psxrecomp_sha256_file(mod_resident_test)
|
||||
if(WIN32)
|
||||
if(MSVC)
|
||||
target_link_options(mod_resident_test PRIVATE /STACK:16777216)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
|
||||
enum class PsxFileHashResult { Ok, OpenError, ReadError };
|
||||
|
||||
// Hash the file's exact bytes, including an empty file. Publish out only on
|
||||
// success. Host crypto accelerates the calculation; unavailable/failed native
|
||||
// hashing restarts with the maintained portable implementation.
|
||||
PsxFileHashResult psx_sha256_file(const std::filesystem::path& path,
|
||||
uint8_t out[32]);
|
||||
@@ -16,6 +16,7 @@ set(PSX_PGO "" CACHE STRING "PGO mode: empty, generate, or use")
|
||||
set_property(CACHE PSX_PGO PROPERTY STRINGS "" generate use)
|
||||
include("${PSXRECOMP_ROOT}/cmake/psx_runtime_ipo.cmake")
|
||||
include("${PSXRECOMP_ROOT}/cmake/psx_execution_profile.cmake")
|
||||
include("${PSXRECOMP_ROOT}/cmake/psx_sha256_file.cmake")
|
||||
|
||||
include("${PSXRECOMP_ROOT}/cmake/psx_dependency_archive.cmake")
|
||||
include("${PSXRECOMP_ROOT}/runtime/chd_dependency.cmake")
|
||||
@@ -1571,6 +1572,7 @@ function(psxrecomp_add_runtime_target target)
|
||||
${PSXRT_EXTRAS_SOURCES}
|
||||
)
|
||||
target_link_libraries(${target} PRIVATE chdr-static)
|
||||
psxrecomp_sha256_file(${target})
|
||||
set(_execution_contract
|
||||
"${PSXRECOMP_ROOT}/runtime/src/gte.cpp"
|
||||
"${PSXRECOMP_ROOT}/runtime/src/pgxp.cpp"
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "psx_memory.h"
|
||||
#include "render_pass_projection.h"
|
||||
#include "psx_sha256.h"
|
||||
#include "psx_sha256_file.h"
|
||||
#include "cpu_state.h"
|
||||
#include "psx_lobby_client.h"
|
||||
|
||||
@@ -351,12 +352,12 @@ bool sha256_file(const std::filesystem::path& path, std::string& out,
|
||||
if (path.empty()) return true;
|
||||
const DiscPathResolution resolved = resolve_disc_path(path);
|
||||
const std::filesystem::path input = resolved.data;
|
||||
psx_sha256_ctx hash;
|
||||
psx_sha256_init(&hash);
|
||||
std::string extension = resolved.mount.extension().string();
|
||||
std::transform(extension.begin(), extension.end(), extension.begin(),
|
||||
[](unsigned char c) { return (char)std::tolower(c); });
|
||||
if (extension == ".chd") {
|
||||
psx_sha256_ctx hash;
|
||||
psx_sha256_init(&hash);
|
||||
PS1::ISOReader disc;
|
||||
if (!disc.Open(resolved.mount.string())) {
|
||||
if (error) *error =
|
||||
@@ -383,24 +384,17 @@ bool sha256_file(const std::filesystem::path& path, std::string& out,
|
||||
return true;
|
||||
}
|
||||
|
||||
std::array<uint8_t, 1024 * 1024> buffer{};
|
||||
std::ifstream file(input, std::ios::binary);
|
||||
if (!file) {
|
||||
uint8_t digest[32];
|
||||
const auto result = psx_sha256_file(input, digest);
|
||||
if (result == PsxFileHashResult::OpenError) {
|
||||
if (error) *error = "cannot fingerprint image: " + input.string();
|
||||
return false;
|
||||
}
|
||||
while (file) {
|
||||
file.read((char*)buffer.data(), (std::streamsize)buffer.size());
|
||||
const std::streamsize got = file.gcount();
|
||||
if (got > 0) psx_sha256_update(&hash, buffer.data(), (size_t)got);
|
||||
}
|
||||
if (!file.eof()) {
|
||||
if (result != PsxFileHashResult::Ok) {
|
||||
if (error) *error =
|
||||
"cannot finish fingerprinting image: " + input.string();
|
||||
return false;
|
||||
}
|
||||
uint8_t digest[32];
|
||||
psx_sha256_final(&hash, digest);
|
||||
std::ostringstream text;
|
||||
for (uint8_t byte : digest)
|
||||
text << std::hex << std::setw(2) << std::setfill('0') << (unsigned)byte;
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
#include "psx_sha256_file.h"
|
||||
#include "psx_sha256.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
|
||||
#if defined(_WIN32) && !defined(PSX_SHA256_FILE_PORTABLE)
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <bcrypt.h>
|
||||
#elif defined(PSX_SHA256_FILE_OPENSSL) && !defined(PSX_SHA256_FILE_PORTABLE)
|
||||
#include <openssl/evp.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
class NativeHash {
|
||||
public:
|
||||
NativeHash() = default;
|
||||
NativeHash(const NativeHash&) = delete;
|
||||
NativeHash& operator=(const NativeHash&) = delete;
|
||||
|
||||
#if defined(_WIN32) && !defined(PSX_SHA256_FILE_PORTABLE)
|
||||
~NativeHash() {
|
||||
if (hash_) BCryptDestroyHash(hash_);
|
||||
if (algorithm_) BCryptCloseAlgorithmProvider(algorithm_, 0);
|
||||
}
|
||||
bool init() {
|
||||
return BCryptOpenAlgorithmProvider(&algorithm_, BCRYPT_SHA256_ALGORITHM,
|
||||
nullptr, 0) >= 0 &&
|
||||
BCryptCreateHash(algorithm_, &hash_, nullptr, 0,
|
||||
nullptr, 0, 0) >= 0;
|
||||
}
|
||||
bool update(const uint8_t* data, size_t size) {
|
||||
// The reader passes at most 1 MiB, within BCrypt's ULONG length.
|
||||
return BCryptHashData(hash_, const_cast<PUCHAR>(data),
|
||||
static_cast<ULONG>(size), 0) >= 0;
|
||||
}
|
||||
bool finish(uint8_t out[32]) {
|
||||
return BCryptFinishHash(hash_, out, 32, 0) >= 0;
|
||||
}
|
||||
private:
|
||||
BCRYPT_ALG_HANDLE algorithm_ = nullptr;
|
||||
BCRYPT_HASH_HANDLE hash_ = nullptr;
|
||||
#elif defined(PSX_SHA256_FILE_OPENSSL) && !defined(PSX_SHA256_FILE_PORTABLE)
|
||||
~NativeHash() { EVP_MD_CTX_free(ctx_); }
|
||||
bool init() {
|
||||
ctx_ = EVP_MD_CTX_new();
|
||||
return ctx_ && EVP_DigestInit_ex(ctx_, EVP_sha256(), nullptr) == 1;
|
||||
}
|
||||
bool update(const uint8_t* data, size_t size) {
|
||||
return EVP_DigestUpdate(ctx_, data, size) == 1;
|
||||
}
|
||||
bool finish(uint8_t out[32]) {
|
||||
unsigned int size = 0;
|
||||
return EVP_DigestFinal_ex(ctx_, out, &size) == 1 && size == 32;
|
||||
}
|
||||
private:
|
||||
EVP_MD_CTX* ctx_ = nullptr;
|
||||
#else
|
||||
bool init() { return false; }
|
||||
bool update(const uint8_t*, size_t) { return false; }
|
||||
bool finish(uint8_t[32]) { return false; }
|
||||
#endif
|
||||
};
|
||||
|
||||
// A backend error may occur after consuming input. Restart the whole stream,
|
||||
// never mix states from different implementations or publish a partial hash.
|
||||
template<class Hash>
|
||||
bool try_native(std::ifstream& file, std::vector<uint8_t>& buffer,
|
||||
uint8_t digest[32], Hash& native) {
|
||||
if (!native.init()) return false;
|
||||
while (file) {
|
||||
file.read(reinterpret_cast<char*>(buffer.data()),
|
||||
static_cast<std::streamsize>(buffer.size()));
|
||||
const auto got = file.gcount();
|
||||
if (got > 0 && !native.update(buffer.data(), static_cast<size_t>(got)))
|
||||
return false;
|
||||
}
|
||||
return !file.bad() && file.eof() && native.finish(digest);
|
||||
}
|
||||
|
||||
template<class Hash>
|
||||
PsxFileHashResult hash_file(const std::filesystem::path& path,
|
||||
uint8_t out[32], Hash& native) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) return PsxFileHashResult::OpenError;
|
||||
std::vector<uint8_t> buffer(1024 * 1024);
|
||||
uint8_t digest[32];
|
||||
if (!try_native(file, buffer, digest, native)) {
|
||||
if (file.bad()) return PsxFileHashResult::ReadError;
|
||||
file.clear();
|
||||
file.seekg(0, std::ios::beg);
|
||||
if (!file) return PsxFileHashResult::ReadError;
|
||||
psx_sha256_ctx portable;
|
||||
psx_sha256_init(&portable);
|
||||
while (file) {
|
||||
file.read(reinterpret_cast<char*>(buffer.data()),
|
||||
static_cast<std::streamsize>(buffer.size()));
|
||||
const auto got = file.gcount();
|
||||
if (got > 0)
|
||||
psx_sha256_update(&portable, buffer.data(), static_cast<size_t>(got));
|
||||
}
|
||||
if (file.bad() || !file.eof()) return PsxFileHashResult::ReadError;
|
||||
psx_sha256_final(&portable, digest);
|
||||
}
|
||||
std::copy(digest, digest + 32, out);
|
||||
return PsxFileHashResult::Ok;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PsxFileHashResult psx_sha256_file(const std::filesystem::path& path,
|
||||
uint8_t out[32]) {
|
||||
NativeHash native;
|
||||
return hash_file(path, out, native);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// Include the maintained implementation to inject backend failures without a
|
||||
// product hook or a second copy of its stream/fallback logic.
|
||||
#include "../src/psx_sha256_file.cpp"
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
static unsigned checks;
|
||||
static void check(bool value, const char* message) {
|
||||
++checks;
|
||||
if (!value) {
|
||||
std::cerr << "FAIL: " << message << '\n';
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
static std::string hex(const uint8_t* digest) {
|
||||
constexpr char alphabet[] = "0123456789abcdef";
|
||||
std::string result;
|
||||
for (size_t i = 0; i < 32; ++i) {
|
||||
result += alphabet[digest[i] >> 4];
|
||||
result += alphabet[digest[i] & 15];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
struct FailedBackend {
|
||||
int stage;
|
||||
unsigned calls = 0;
|
||||
bool failed = false;
|
||||
bool init() { return !(failed = stage == 0); }
|
||||
bool update(const uint8_t*, size_t) {
|
||||
++calls;
|
||||
return !(failed = stage == 1 && calls == 2);
|
||||
}
|
||||
bool finish(uint8_t out[32]) {
|
||||
std::fill(out, out + 32, 0x19); // failure must never publish this
|
||||
failed = true;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// Optional private file probe: one real host backend or portable build,
|
||||
// suitable for separate isolated throughput measurements.
|
||||
if (argc == 2) {
|
||||
uint8_t digest[32];
|
||||
const auto begin = std::chrono::steady_clock::now();
|
||||
const auto result = psx_sha256_file(std::filesystem::u8path(argv[1]), digest);
|
||||
const double seconds = std::chrono::duration<double>(
|
||||
std::chrono::steady_clock::now() - begin).count();
|
||||
if (result != PsxFileHashResult::Ok) return 2;
|
||||
std::cout << hex(digest) << " " << seconds << '\n';
|
||||
return 0;
|
||||
}
|
||||
|
||||
#if !defined(PSX_SHA256_FILE_PORTABLE) && (defined(_WIN32) || defined(PSX_SHA256_FILE_OPENSSL))
|
||||
NativeHash available;
|
||||
check(available.init(), "native backend is available on qualification host");
|
||||
#endif
|
||||
|
||||
const auto unique = std::chrono::high_resolution_clock::now().time_since_epoch().count();
|
||||
const auto root = std::filesystem::temp_directory_path() /
|
||||
("psx-sha256-" + std::to_string(unique));
|
||||
check(std::filesystem::create_directory(root), "private fixture directory");
|
||||
const auto path = root / std::filesystem::u8path(u8"disc-\u00e9-\u65e5.bin");
|
||||
std::vector<uint8_t> bytes;
|
||||
constexpr size_t lengths[] = {
|
||||
0, 1, 3, 55, 56, 63, 64, 65, 119, 120, 127, 128, 129,
|
||||
1048575, 1048576, 1048577, 2097169};
|
||||
uint32_t rng = 0x53484132;
|
||||
for (size_t size : lengths) {
|
||||
bytes.resize(size);
|
||||
for (auto& byte : bytes) {
|
||||
rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5;
|
||||
byte = static_cast<uint8_t>(rng);
|
||||
}
|
||||
{
|
||||
std::ofstream file(path, std::ios::binary | std::ios::trunc);
|
||||
if (!bytes.empty()) file.write(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||||
check(static_cast<bool>(file), "write fixture");
|
||||
}
|
||||
std::array<uint8_t, 32> expected{}, observed{};
|
||||
psx_sha256_compute(bytes.data(), bytes.size(), expected.data());
|
||||
check(psx_sha256_file(path, observed.data()) == PsxFileHashResult::Ok,
|
||||
"hash file");
|
||||
check(observed == expected, "native/portable exact digest");
|
||||
if (!size) check(hex(observed.data()) ==
|
||||
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"standard empty vector");
|
||||
}
|
||||
// The largest vector consumes more than one native chunk before fallback.
|
||||
std::array<uint8_t, 32> expected{}, observed{};
|
||||
psx_sha256_compute(bytes.data(), bytes.size(), expected.data());
|
||||
for (int stage = 0; stage < 3; ++stage) {
|
||||
FailedBackend backend{stage};
|
||||
check(hash_file(path, observed.data(), backend) == PsxFileHashResult::Ok,
|
||||
"native failure falls back");
|
||||
check(backend.failed && observed == expected, "fallback restarts at byte zero");
|
||||
if (stage == 1) check(backend.calls == 2, "injected partial update failure");
|
||||
}
|
||||
{
|
||||
std::ofstream file(path, std::ios::binary | std::ios::trunc);
|
||||
file << "abc";
|
||||
}
|
||||
check(psx_sha256_file(path, observed.data()) == PsxFileHashResult::Ok &&
|
||||
hex(observed.data()) ==
|
||||
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
|
||||
"standard abc vector");
|
||||
observed.fill(0xa5);
|
||||
expected = observed;
|
||||
check(psx_sha256_file(root / "absent", observed.data()) == PsxFileHashResult::OpenError,
|
||||
"missing file rejected");
|
||||
check(observed == expected, "missing file leaves output untouched");
|
||||
check(psx_sha256_file(root, observed.data()) != PsxFileHashResult::Ok,
|
||||
"directory rejected");
|
||||
check(observed == expected, "unreadable file leaves output untouched");
|
||||
// Delete only our two exact fixture paths; no recursive cleanup.
|
||||
check(std::filesystem::remove(path), "remove fixture file");
|
||||
check(std::filesystem::remove(root), "remove empty fixture directory");
|
||||
std::cout << checks << " SHA-256 file checks passed\n";
|
||||
}
|
||||
Reference in New Issue
Block a user