mirror of
https://github.com/blackbearreloaded/ProsperoEden.git
synced 2026-10-19 05:09:50 -05:00
Four fixes to Eden's own source, applied when make prepare extracts it (#191)
tools/eden-patches/*.diff are applied in name order to the pinned Eden tree, each once: a patch that reverses cleanly is already in. One that neither reverses nor applies was changed after an earlier version went in, and the files it touches come back from the pinned archive first. - 01 fast file reads: read-only files are served from recently read 1 MiB blocks. Each system call on the console's storage costs about the same whatever its size, so a game's small reads no longer pay it each. Writable files keep stdio's buffered view, which has their unflushed writes. - 02 cache storage per title: a cache storage request with title id 0 means the current process, as switchbrew documents for save data. Otherwise every game's cache storage was one shared folder, and two games of one series writing the same file names read each other's GPU shader caches. - 03 bounded shader local memory: an out-of-bounds local memory load reads zero and a store is dropped, both through word 0. The PS5 faults on scratch accesses outside the allocation, which killed the process when a game indexed a local array with -1 after its intro. - 04 CPU reads without reactive flushing: plain CPU reads of GPU-cached pages download GPU-written data only when reactive flushing is on, as with fastmem, where such reads never trap. Without fastmem every read otherwise took the rasterizer caches' locks, one 4 KiB page at a time. docs/BUILDING.md says that the fork now patches these files. Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
1 parent
64a1ee754a
commit
a302cde87a
6 files changed
+297
-3
No files matched your search
+6
-3
@@ -79,9 +79,12 @@ Downloads are cached in `~/.cache/prosperoeden-deps` (`PROSPEROEDEN_DEPS_CACHE`)
|
||||
Inside this repository, in `.deps/`:
|
||||
|
||||
- **Eden** at commit `67bada77f8a43a90da2e94e89b8e7da73c256989` (GitHub mirror archive), with
|
||||
Eden's own hash-pinned packages, which its configure step downloads. ProsperoEden does not
|
||||
modify Eden's files: the PS5 frontend in `headless/` replaces and derives sources at
|
||||
configure time (`headless/inject.cmake`).
|
||||
Eden's own hash-pinned packages, which its configure step downloads. The PS5 frontend in
|
||||
`headless/` replaces and derives sources at configure time (`headless/inject.cmake`). This
|
||||
fork also patches a few of Eden's files when `make prepare` extracts them
|
||||
(`tools/eden-patches/*.diff`, in name order, each applied once): faster reads of game files,
|
||||
per-game cache storage, bounds-checked shader local memory, and CPU reads of GPU-cached
|
||||
pages without reactive flushing.
|
||||
- **FFmpeg** at the commit Eden pins, built with only the decoders games use.
|
||||
- **PS5 OpenGL 4.6 SDK 1.0.2** (release archive), for the launcher and the OpenGL renderer.
|
||||
- **OpenSSL, zlib, libcurl and libpsl** from pacbrew v0.40.2.
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
--- a/src/common/fs/file.h
|
||||
+++ b/src/common/fs/file.h
|
||||
@@ -289,6 +289,14 @@
|
||||
}
|
||||
|
||||
/**
|
||||
+ * Reads length bytes at offset without the stdio buffer (which splits a read into
|
||||
+ * buffer-sized system calls) and without moving the file pointer.
|
||||
+ *
|
||||
+ * @returns Count of bytes successfully read.
|
||||
+ */
|
||||
+ [[nodiscard]] size_t ReadAt(void* data, size_t length, s64 offset) const;
|
||||
+
|
||||
+ /**
|
||||
* Writes a span of T data to a file sequentially.
|
||||
* This function writes from the current position of the file pointer and
|
||||
* advances it by the (count of T * sizeof(T)) bytes successfully written.
|
||||
--- a/src/common/fs/file.cpp
|
||||
+++ b/src/common/fs/file.cpp
|
||||
@@ -427,6 +427,33 @@
|
||||
return file_size;
|
||||
}
|
||||
|
||||
+size_t IOFile::ReadAt(void* data, size_t length, s64 offset) const {
|
||||
+ if (!IsOpen()) {
|
||||
+ return 0;
|
||||
+ }
|
||||
+#ifdef _WIN32
|
||||
+ if (fseeko(file, offset, SEEK_SET) != 0) {
|
||||
+ return 0;
|
||||
+ }
|
||||
+ return std::fread(data, 1, length, file);
|
||||
+#else
|
||||
+ const int fd = fileno(file);
|
||||
+ size_t done = 0;
|
||||
+ while (done < length) {
|
||||
+ const auto got = pread(fd, static_cast<u8*>(data) + done, length - done,
|
||||
+ static_cast<off_t>(offset + static_cast<s64>(done)));
|
||||
+ if (got < 0 && errno == EINTR) {
|
||||
+ continue;
|
||||
+ }
|
||||
+ if (got <= 0) {
|
||||
+ break;
|
||||
+ }
|
||||
+ done += static_cast<size_t>(got);
|
||||
+ }
|
||||
+ return done;
|
||||
+#endif
|
||||
+}
|
||||
+
|
||||
bool IOFile::Seek(s64 offset, SeekOrigin origin) const {
|
||||
if (!IsOpen()) {
|
||||
return false;
|
||||
--- a/src/core/file_sys/vfs/vfs_real.h
|
||||
+++ b/src/core/file_sys/vfs/vfs_real.h
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
+#include <vector>
|
||||
#include "common/intrusive_list.h"
|
||||
#include "core/file_sys/fs_filesystem.h"
|
||||
#include "core/file_sys/vfs/vfs.h"
|
||||
@@ -22,6 +23,15 @@
|
||||
|
||||
struct FileReference : public Common::IntrusiveListBaseNode<FileReference> {
|
||||
std::shared_ptr<Common::FS::IOFile> file{};
|
||||
+ // Read-only files: recently read 1 MiB blocks. Each system call on the console's storage
|
||||
+ // costs about the same whatever its size, so a game's small reads are served from whole blocks.
|
||||
+ struct Block {
|
||||
+ u64 index;
|
||||
+ u64 used;
|
||||
+ std::vector<u8> data;
|
||||
+ };
|
||||
+ std::vector<Block> blocks{};
|
||||
+ u64 block_clock{};
|
||||
};
|
||||
|
||||
class RealVfsFile;
|
||||
--- a/src/core/file_sys/vfs/vfs_real.cpp
|
||||
+++ b/src/core/file_sys/vfs/vfs_real.cpp
|
||||
@@ -6,9 +6,11 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
+#include <cstring>
|
||||
#include <iterator>
|
||||
#include <utility>
|
||||
#include "common/assert.h"
|
||||
+#include "common/literals.h"
|
||||
#include "common/fs/file.h"
|
||||
#include "common/fs/fs.h"
|
||||
#include "common/fs/path_util.h"
|
||||
@@ -332,12 +334,60 @@
|
||||
return True(perms & OpenMode::Read);
|
||||
}
|
||||
|
||||
+using namespace Common::Literals;
|
||||
+
|
||||
+namespace {
|
||||
+constexpr std::size_t ReadBlockSize = 1_MiB;
|
||||
+constexpr std::size_t ReadBlocksPerFile = 8;
|
||||
+} // namespace
|
||||
+
|
||||
std::size_t RealVfsFile::Read(u8* data, std::size_t length, std::size_t offset) const {
|
||||
auto lk = base.RefreshReference(path, perms, *reference);
|
||||
- if (!reference->file || !reference->file->Seek(static_cast<s64>(offset))) {
|
||||
+ if (!reference->file) {
|
||||
return 0;
|
||||
}
|
||||
- return reference->file->ReadSpan(std::span{data, length});
|
||||
+ if (True(perms & OpenMode::Write)) {
|
||||
+ // Writable files keep stdio's buffered view, which has their unflushed writes.
|
||||
+ if (!reference->file->Seek(static_cast<s64>(offset))) {
|
||||
+ return 0;
|
||||
+ }
|
||||
+ return reference->file->ReadSpan(std::span{data, length});
|
||||
+ }
|
||||
+ if (length >= ReadBlockSize) {
|
||||
+ return reference->file->ReadAt(data, length, static_cast<s64>(offset));
|
||||
+ }
|
||||
+ std::size_t done = 0;
|
||||
+ while (done < length) {
|
||||
+ const u64 position = offset + done;
|
||||
+ const u64 index = position / ReadBlockSize;
|
||||
+ auto& blocks = reference->blocks;
|
||||
+ auto block = std::find_if(blocks.begin(), blocks.end(),
|
||||
+ [index](const auto& entry) { return entry.index == index; });
|
||||
+ if (block == blocks.end()) {
|
||||
+ if (blocks.size() < ReadBlocksPerFile) {
|
||||
+ block = blocks.emplace(blocks.end());
|
||||
+ } else {
|
||||
+ block = std::min_element(blocks.begin(), blocks.end(),
|
||||
+ [](const auto& a, const auto& b) { return a.used < b.used; });
|
||||
+ }
|
||||
+ block->index = index;
|
||||
+ block->data.resize(ReadBlockSize);
|
||||
+ block->data.resize(reference->file->ReadAt(block->data.data(), ReadBlockSize,
|
||||
+ static_cast<s64>(index * ReadBlockSize)));
|
||||
+ }
|
||||
+ block->used = ++reference->block_clock;
|
||||
+ const u64 start = position - index * ReadBlockSize;
|
||||
+ if (start >= block->data.size()) {
|
||||
+ break;
|
||||
+ }
|
||||
+ const std::size_t count = std::min<std::size_t>(length - done, block->data.size() - start);
|
||||
+ std::memcpy(data + done, block->data.data() + start, count);
|
||||
+ done += count;
|
||||
+ if (block->data.size() < ReadBlockSize) {
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+ return done;
|
||||
}
|
||||
|
||||
std::size_t RealVfsFile::Write(const u8* data, std::size_t length, std::size_t offset) {
|
||||
@@ -0,0 +1,16 @@
|
||||
--- a/src/core/file_sys/savedata_factory.cpp
|
||||
+++ b/src/core/file_sys/savedata_factory.cpp
|
||||
@@ -109,8 +109,11 @@
|
||||
SaveDataSpaceId space, SaveDataType type, u64 title_id,
|
||||
u128 user_id, u64 save_id) {
|
||||
// According to switchbrew, if a save is of type SaveData and the title id field is 0, it should
|
||||
- // be interpreted as the title id of the current process.
|
||||
- if (type == SaveDataType::Account || type == SaveDataType::Device) {
|
||||
+ // be interpreted as the title id of the current process. The same holds for cache storage:
|
||||
+ // otherwise every game's cache storage is one shared folder (save/cache/0000000000000000), and
|
||||
+ // two games writing the same file names there (two games of one series sharing a folder name) read
|
||||
+ // each other's GPU shader caches.
|
||||
+ if (type == SaveDataType::Account || type == SaveDataType::Device || type == SaveDataType::Cache) {
|
||||
if (title_id == 0) {
|
||||
title_id = program_id;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
--- a/src/shader_recompiler/frontend/maxwell/translate/impl/load_store_local_shared.cpp
|
||||
+++ b/src/shader_recompiler/frontend/maxwell/translate/impl/load_store_local_shared.cpp
|
||||
@@ -88,10 +88,31 @@
|
||||
return ir.BitwiseAnd(ir.ShiftLeftLogical(offset, ir.Imm32(3)), ir.Imm32(16));
|
||||
}
|
||||
|
||||
-IR::U32 LoadLocal(TranslatorVisitor& v, const IR::U32& word_offset, const IR::U32& offset) {
|
||||
+IR::U1 LocalInBounds(TranslatorVisitor& v, const IR::U32& offset) {
|
||||
const IR::U32 local_memory_size{v.ir.Imm32(v.env.LocalMemorySize())};
|
||||
- const IR::U1 in_bounds{v.ir.ILessThan(offset, local_memory_size, false)};
|
||||
- return IR::U32{v.ir.Select(in_bounds, v.ir.LoadLocal(word_offset), v.ir.Imm32(0))};
|
||||
+ return v.ir.ILessThan(offset, local_memory_size, false);
|
||||
+}
|
||||
+
|
||||
+// An out of bounds offset reads as zero. The load itself goes to word 0, so it never leaves
|
||||
+// the local memory: hosts that fault on scratch accesses outside it (PS5) would kill the
|
||||
+// process, e.g. a game indexing a local array with -1 after its intro.
|
||||
+IR::U32 LoadLocal(TranslatorVisitor& v, const IR::U32& word_offset, const IR::U32& offset) {
|
||||
+ const IR::U1 in_bounds{LocalInBounds(v, offset)};
|
||||
+ const IR::U32 safe_word_offset{v.ir.Select(in_bounds, word_offset, v.ir.Imm32(0))};
|
||||
+ return IR::U32{v.ir.Select(in_bounds, v.ir.LoadLocal(safe_word_offset), v.ir.Imm32(0))};
|
||||
+}
|
||||
+
|
||||
+// An out of bounds store is dropped: word 0 is rewritten with its own value.
|
||||
+void WriteLocal(TranslatorVisitor& v, const IR::U32& word_offset, const IR::U32& offset,
|
||||
+ const IR::U32& value) {
|
||||
+ if (offset.IsImmediate()) {
|
||||
+ v.ir.WriteLocal(word_offset, value);
|
||||
+ return;
|
||||
+ }
|
||||
+ const IR::U1 in_bounds{LocalInBounds(v, offset)};
|
||||
+ const IR::U32 safe_word_offset{v.ir.Select(in_bounds, word_offset, v.ir.Imm32(0))};
|
||||
+ const IR::U32 safe_value{v.ir.Select(in_bounds, value, v.ir.LoadLocal(safe_word_offset))};
|
||||
+ v.ir.WriteLocal(safe_word_offset, safe_value);
|
||||
}
|
||||
} // Anonymous namespace
|
||||
|
||||
@@ -166,14 +187,14 @@
|
||||
switch (bit_size) {
|
||||
case 8: {
|
||||
const IR::U32 bit{ByteOffset(ir, offset)};
|
||||
- const IR::U32 value{ir.BitFieldInsert(ir.LoadLocal(word_offset), src, bit, ir.Imm32(8))};
|
||||
- ir.WriteLocal(word_offset, value);
|
||||
+ const IR::U32 value{ir.BitFieldInsert(LoadLocal(*this, word_offset, offset), src, bit, ir.Imm32(8))};
|
||||
+ WriteLocal(*this, word_offset, offset, value);
|
||||
break;
|
||||
}
|
||||
case 16: {
|
||||
const IR::U32 bit{ShortOffset(ir, offset)};
|
||||
- const IR::U32 value{ir.BitFieldInsert(ir.LoadLocal(word_offset), src, bit, ir.Imm32(16))};
|
||||
- ir.WriteLocal(word_offset, value);
|
||||
+ const IR::U32 value{ir.BitFieldInsert(LoadLocal(*this, word_offset, offset), src, bit, ir.Imm32(16))};
|
||||
+ WriteLocal(*this, word_offset, offset, value);
|
||||
break;
|
||||
}
|
||||
case 32:
|
||||
@@ -182,9 +203,10 @@
|
||||
if (!IR::IsAligned(reg, static_cast<size_t>(bit_size / 32))) {
|
||||
throw NotImplementedException("Unaligned source register");
|
||||
}
|
||||
- ir.WriteLocal(word_offset, src);
|
||||
+ WriteLocal(*this, word_offset, offset, src);
|
||||
for (int i = 1; i < bit_size / 32; ++i) {
|
||||
- ir.WriteLocal(ir.IAdd(word_offset, ir.Imm32(i)), X(reg + i));
|
||||
+ WriteLocal(*this, ir.IAdd(word_offset, ir.Imm32(i)), ir.IAdd(offset, ir.Imm32(i * 4)),
|
||||
+ X(reg + i));
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
--- a/src/core/memory.cpp
|
||||
+++ b/src/core/memory.cpp
|
||||
@@ -292,7 +292,7 @@
|
||||
},
|
||||
[&](const Common::ProcessAddress current_vaddr, const std::size_t offset, const std::size_t copy_amount, const u8* const ptr) {
|
||||
if (!unsafe) {
|
||||
- HandleRasterizerDownload(GetInteger(current_vaddr), copy_amount);
|
||||
+ HandleRasterizerReadDownload(GetInteger(current_vaddr), copy_amount);
|
||||
}
|
||||
std::memcpy(reinterpret_cast<u8*>(buffer) + offset, ptr, copy_amount);
|
||||
});
|
||||
@@ -625,7 +625,7 @@
|
||||
if (auto const ptr = GetPointerImpl(addr, [addr]() {
|
||||
LOG_ERROR(HW_Memory, "Unmapped Read{} @ {:#016x}", sizeof(T) * 8, addr);
|
||||
}, [&]() {
|
||||
- HandleRasterizerDownload(addr, sizeof(T));
|
||||
+ HandleRasterizerReadDownload(addr, sizeof(T));
|
||||
}); ptr) [[likely]] {
|
||||
// It may be tempting to rewrite this particular section to use "reinterpret_cast";
|
||||
// afterall, it's trivially copyable so surely it can be copied ov- Alignment.
|
||||
@@ -678,6 +678,16 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
+ // Plain CPU reads of GPU-cached pages. Without reactive flushing they do not download
|
||||
+ // GPU-written data, as with fastmem (where such reads never trap); without fastmem every
|
||||
+ // read otherwise takes the rasterizer caches' locks, one 4 KiB page at a time.
|
||||
+ void HandleRasterizerReadDownload(VAddr v_address, size_t size) {
|
||||
+ if (!Settings::values.use_reactive_flushing.GetValue()) {
|
||||
+ return;
|
||||
+ }
|
||||
+ HandleRasterizerDownload(v_address, size);
|
||||
+ }
|
||||
+
|
||||
void HandleRasterizerDownload(VAddr v_address, size_t size) {
|
||||
const auto* p = GetPointerImpl(
|
||||
v_address, []() {}, []() {});
|
||||
@@ -37,6 +37,19 @@ if [[ ! -f $eden/CMakeLists.txt || $(cat "$eden/.unpacked-from" 2>/dev/null) !=
|
||||
fi
|
||||
printf '%s\n' '67bada77f8a43a90da2e94e89b8e7da73c256989' > "$eden/GIT-COMMIT"
|
||||
printf '%s\n' 'ps5-headless' > "$eden/GIT-REFSPEC"
|
||||
# Fixes to Eden's own source (tools/eden-patches, in name order), each applied once: a patch that
|
||||
# reverses cleanly is already in the tree. One that neither reverses nor applies was changed after
|
||||
# an earlier version of it went in: the files it touches come back from the pinned archive first.
|
||||
command -v patch >/dev/null || { echo "patch is required (apt install patch)" >&2; exit 1; }
|
||||
for eden_patch in tools/eden-patches/*.diff; do
|
||||
if patch -d "$eden" -p1 -R --dry-run -s -f < "$eden_patch" >/dev/null 2>&1; then continue; fi
|
||||
step "Eden patch $(basename "$eden_patch")"
|
||||
if ! patch -d "$eden" -p1 -N --dry-run -s -f < "$eden_patch" >/dev/null 2>&1; then
|
||||
sed -n 's|^+++ b/||p' "$eden_patch" | sed 's|^|*/|' |
|
||||
xargs tar -xzf ".deps/$eden_archive" --wildcards --strip-components=1 -C "$eden"
|
||||
fi
|
||||
patch -d "$eden" -p1 -N -s < "$eden_patch" || { echo "$eden_patch does not apply to Eden's source" >&2; exit 1; }
|
||||
done
|
||||
# Optional: seed Eden's package cache from another checkout's (CI reuses a development cache).
|
||||
if [[ -n ${EDEN_CPM_CACHE_SEED:-} && ! -d $eden/.cache/cpm ]]; then
|
||||
step "Eden package cache from $EDEN_CPM_CACHE_SEED"
|
||||
|
||||
Reference in new issue
Block a user