Accelerate exact startup file hashing with native host crypto

This commit is contained in:
Matthew Stanley
2026-10-06 20:00:55 -07:00
parent b389f9edf0
commit 1d201aa39f
8 changed files with 334 additions and 15 deletions
+19
View File
@@ -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()
+27
View File
@@ -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
View File
@@ -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)
+12
View File
@@ -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]);
+2
View File
@@ -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"
+7 -13
View File
@@ -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;
+120
View File
@@ -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);
}
+125
View File
@@ -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";
}