mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-09-03 11:15:20 +02:00
GetCurrentDrawAsDebugVertices (GE debugger vertex preview) sized its index scratch
buffer at a fixed 65536 and then ran both expanding steps into it: index generation
turns strips/fans into up to 3 indices per input index, and RunSoftwareTransform can
then expand points/lines/rects into 6 more each. A 30000-vertex triangle strip wrote
~90000 entries. Size the buffer from the count instead.
The Expand{Rectangles,Lines,Points} capacity checks were also off: they compared the
expansion against indsSize but write the expanded indices at inds + vertexCount, so
the input count has to be part of the sum.
ControlMapper::Axis wrote rawAxisValue_[axis.axisId] with no bounds check, one line
below an explicit check on axis.deviceId. axisId comes straight from the device -
Android reports AXIS_GENERIC_13..16 as 44..47, against a 44-entry array - so it wrote
into the neighbouring deviceTimestamps_. NativeAxis had the same unchecked write into
HLEPlugins::PluginDataAxis, where it goes out of the object entirely.
Rewind's LockedDecompress computed its copy-from-base block size as
base.size() - result.size() in size_t and truncated to int, so it went negative once
the output grew past the base, and insert() then ran with last < first. That happens
because a state can outlive the base it was compressed against: there are 20 states
but only 2 bases, rotated every 16 saves. Track a generation per base and refuse to
decode a state whose base is gone, and bound the block size against the base itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DCPmm7FoQUoqrbMdhfqhQ2
206 lines
6.1 KiB
C++
206 lines
6.1 KiB
C++
#include "Common/Thread/ThreadUtil.h"
|
|
#include "Common/Data/Text/I18n.h"
|
|
#include "Common/StringUtils.h"
|
|
#include "Core/SaveState.h"
|
|
#include "Core/SaveStateRewind.h"
|
|
#include "Core/Core.h"
|
|
#include "Core/Config.h"
|
|
|
|
namespace SaveState {
|
|
|
|
CChunkFileReader::Error StateRingbuffer::Save() {
|
|
rewindLastTime_ = time_now_d();
|
|
|
|
// Make sure we're not processing a previous save. That'll cause a hitch though, but at least won't
|
|
// crash due to contention over buffer_.
|
|
if (compressThread_.joinable())
|
|
compressThread_.join();
|
|
|
|
std::lock_guard<std::mutex> guard(lock_);
|
|
|
|
int n = next_++ % size_;
|
|
if ((next_ % size_) == first_)
|
|
++first_;
|
|
|
|
std::vector<u8> *compressBuffer = &buffer_;
|
|
CChunkFileReader::Error err;
|
|
|
|
if (base_ == -1 || ++baseUsage_ > BASE_USAGE_INTERVAL)
|
|
{
|
|
base_ = (base_ + 1) % ARRAY_SIZE(bases_);
|
|
baseUsage_ = 0;
|
|
baseGeneration_[base_] = nextBaseGeneration_++;
|
|
err = SaveToRam(bases_[base_]);
|
|
// Let's not bother savestating twice.
|
|
compressBuffer = &bases_[base_];
|
|
} else
|
|
err = SaveToRam(buffer_);
|
|
|
|
if (err == CChunkFileReader::ERROR_NONE) {
|
|
ScheduleCompress(&states_[n].stateBuffer, compressBuffer, &bases_[base_]);
|
|
states_[n].savedTime = time_now_d();
|
|
} else {
|
|
states_[n].clear();
|
|
}
|
|
|
|
baseMapping_[n] = baseGeneration_[base_];
|
|
return err;
|
|
}
|
|
|
|
CChunkFileReader::Error StateRingbuffer::Restore(std::string *errorString, std::string *metadata) {
|
|
std::lock_guard<std::mutex> guard(lock_);
|
|
|
|
// No valid states left.
|
|
if (Empty())
|
|
return CChunkFileReader::ERROR_BAD_FILE;
|
|
|
|
int n = (--next_ + size_) % size_;
|
|
if (states_[n].empty())
|
|
return CChunkFileReader::ERROR_BAD_FILE;
|
|
|
|
auto pa = GetI18NCategory(I18NCat::PAUSE);
|
|
|
|
const int generation = baseMapping_[n];
|
|
const int baseSlot = generation < 0 ? -1 : generation % (int)ARRAY_SIZE(bases_);
|
|
if (baseSlot < 0 || baseGeneration_[baseSlot] != generation) {
|
|
// The base this state was compressed against has since been overwritten, so it can't be
|
|
// decoded any more. Only two bases are kept, but the state ring is longer.
|
|
WARN_LOG(Log::SaveState, "Rewind: state %d was compressed against a base that's gone", n);
|
|
return CChunkFileReader::ERROR_BAD_FILE;
|
|
}
|
|
|
|
static std::vector<u8> buffer;
|
|
LockedDecompress(buffer, states_[n].stateBuffer, bases_[baseSlot]);
|
|
CChunkFileReader::Error error = LoadFromRam(buffer, errorString);
|
|
*metadata = pa->T("Rewind");
|
|
|
|
if (states_[n].savedTime) {
|
|
auto di = GetI18NCategory(I18NCat::DIALOG);
|
|
metadata->append(" (");
|
|
metadata->append(ApplySafeSubstitutions(di->T("%1 seconds ago"), static_cast<int>(time_now_d() - states_[n].savedTime)));
|
|
metadata->append(")");
|
|
}
|
|
|
|
rewindLastTime_ = time_now_d();
|
|
return error;
|
|
}
|
|
|
|
void StateRingbuffer::ScheduleCompress(std::vector<u8> *result, const std::vector<u8> *state, const std::vector<u8> *base) {
|
|
if (compressThread_.joinable())
|
|
compressThread_.join();
|
|
compressThread_ = std::thread([=] {
|
|
SetCurrentThreadName("SaveStateCompress");
|
|
|
|
// Should do no I/O, so no JNI thread context needed.
|
|
Compress(*result, *state, *base);
|
|
});
|
|
}
|
|
|
|
void StateRingbuffer::Compress(std::vector<u8> &result, const std::vector<u8> &state, const std::vector<u8> &base) {
|
|
std::lock_guard<std::mutex> guard(lock_);
|
|
// Bail if we were cleared before locking.
|
|
if (first_ == 0 && next_ == 0)
|
|
return;
|
|
|
|
double start_time = time_now_d();
|
|
result.clear();
|
|
result.reserve(512 * 1024);
|
|
for (size_t i = 0; i < state.size(); i += BLOCK_SIZE)
|
|
{
|
|
int blockSize = std::min(BLOCK_SIZE, (int)(state.size() - i));
|
|
if (i + blockSize > base.size() || memcmp(&state[i], &base[i], blockSize) != 0) {
|
|
result.push_back(1);
|
|
result.insert(result.end(), state.begin() + i, state.begin() + i + blockSize);
|
|
} else {
|
|
result.push_back(0);
|
|
}
|
|
}
|
|
|
|
double taken_s = time_now_d() - start_time;
|
|
DEBUG_LOG(Log::SaveState, "Rewind: Compressed save from %d bytes to %d in %0.2f ms.", (int)state.size(), (int)result.size(), taken_s * 1000.0);
|
|
}
|
|
|
|
void StateRingbuffer::LockedDecompress(std::vector<u8> &result, const std::vector<u8> &compressed, const std::vector<u8> &base) {
|
|
result.clear();
|
|
result.reserve(base.size());
|
|
auto basePos = base.begin();
|
|
for (size_t i = 0; i < compressed.size(); ) {
|
|
if (compressed[i] == 0) {
|
|
++i;
|
|
// Bound against what's actually left of the base: the subtraction this used to do
|
|
// (base.size() - result.size()) wraps once the output is longer than the base.
|
|
const int blockSize = (int)std::min((size_t)BLOCK_SIZE, (size_t)(base.end() - basePos));
|
|
if (blockSize <= 0) {
|
|
break;
|
|
}
|
|
result.insert(result.end(), basePos, basePos + blockSize);
|
|
basePos += blockSize;
|
|
} else {
|
|
++i;
|
|
int blockSize = std::min(BLOCK_SIZE, (int)(compressed.size() - i));
|
|
result.insert(result.end(), compressed.begin() + i, compressed.begin() + i + blockSize);
|
|
i += blockSize;
|
|
// This check is to avoid advancing basePos out of range, which MSVC catches.
|
|
// When this happens, we're at the end of decoding anyway.
|
|
if (base.end() - basePos >= blockSize) {
|
|
basePos += blockSize;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void StateRingbuffer::Clear() {
|
|
if (compressThread_.joinable())
|
|
compressThread_.join();
|
|
|
|
// This lock is mainly for shutdown.
|
|
std::lock_guard<std::mutex> guard(lock_);
|
|
first_ = 0;
|
|
next_ = 0;
|
|
for (auto &b : bases_) {
|
|
b.clear();
|
|
}
|
|
for (int &g : baseGeneration_) {
|
|
g = -1;
|
|
}
|
|
nextBaseGeneration_ = 0;
|
|
baseMapping_.clear();
|
|
baseMapping_.resize(size_);
|
|
for (auto &s : states_) {
|
|
s.clear();
|
|
}
|
|
buffer_.clear();
|
|
base_ = -1;
|
|
baseUsage_ = 0;
|
|
rewindLastTime_ = time_now_d();
|
|
}
|
|
|
|
void StateRingbuffer::Process() {
|
|
if (g_Config.iRewindSnapshotInterval <= 0) {
|
|
return;
|
|
}
|
|
if (coreState != CORE_RUNNING_CPU) {
|
|
return;
|
|
}
|
|
|
|
// For fast-forwarding, otherwise they may be useless and too close.
|
|
double now = time_now_d();
|
|
double diff = now - rewindLastTime_;
|
|
if (diff < g_Config.iRewindSnapshotInterval)
|
|
return;
|
|
|
|
DEBUG_LOG(Log::SaveState, "Saving rewind state");
|
|
Save();
|
|
}
|
|
|
|
void StateRingbuffer::NotifyState() {
|
|
// Prevent saving snapshots immediately after loading or saving a state.
|
|
rewindLastTime_ = time_now_d();
|
|
}
|
|
|
|
double StateRingbuffer::NextStateTimestamp() const {
|
|
return rewindLastTime_ + g_Config.iRewindSnapshotInterval;
|
|
}
|
|
|
|
} // namespace SaveState
|