mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
* Libretro: Scaffold yaps2_libretro core (M1: builds + dlopens)
New pcsx2-libretro/ target producing yaps2_libretro.so (ENABLE_LIBRETRO=ON,
or built on demand). Milestone 1 of the libretro port:
- Full libretro v1 entry-point surface, version-script-restricted to
retro_* exports (PCSX2 internals must not collide with the frontend).
- Host:: implementations adapted from pcsx2-sdl: same CPU-thread state
machine (VMManager::Execute on a dedicated thread, RunOnCPUThread queue),
no windowing/clipboard/file-picker.
- retro_load_game boots the VM headlessly: GS renderer forced to Null,
Surfaceless WindowInfo, null audio; config+data self-contained under
<system dir>/pcsx2 (yaps2-libretro.ini).
- retro_run presents a placeholder XRGB8888 frame; no pacing yet.
- POSITION_INDEPENDENT_CODE forced ON (the pcsx2-sdl ET_EXEC persisted-JIT
trick cannot apply to a shared core).
Verified: builds in org.kde.Sdk 6.10 (gcc), dlopen + retro_api_version +
retro_get_system_info OK.
Next: M2 Vulkan negotiation interface (wrap vkCreateInstance/Device so
GSDeviceVK inits against the frontend-shared device, set_image handoff),
M3 frame pacing + libretro input/audio, M4 savestates/disk control.
* Libretro: M2 Vulkan context sharing + frame handoff (first light)
The lrps2-libretro pattern ported to the modern GSDeviceVK:
- VKLibretro.{h,cpp}: the loader's global vkGetInstanceProcAddr is swapped
for a wrapper that intercepts vkCreateDevice (merges the frontend's
required extensions/layers/features into GS's create info and captures
the shared VkDevice) and vkQueueSubmit (serialises against the frontend
through the HW-render interface queue lock).
- GSDeviceVK: adopts the negotiation-provided VkInstance/VkPhysicalDevice
instead of creating its own (and never destroys the frontend's instance);
the surfaceless BeginPresent branch publishes the merged display texture
(ShaderReadOnly + ExecuteCommandBuffer) into a mutex-guarded slot.
- Main.cpp: RETRO_HW_CONTEXT_VULKAN + context negotiation interface
(create_device opens MTGS from the frontend thread, so GSDeviceVK is
fully constructed before the context reply); the VM boot parks until
context_reset delivers the retro_hw_render_interface_vulkan; retro_run
consumes at most one published frame per call and forwards it via
set_image + video_cb(RETRO_HW_FRAME_BUFFER_VALID), dupe otherwise.
First light verified on Turnip Adreno 618 (and llvmpipe): GT3 boots
(VMManager::Initialize StartupSuccess), frames arrive hw_valid with a
non-black 640x448 readback.
Known gaps for M3: no frame pacing (VM free-runs; YAPS2_RUN_SLEEP=1 paces
headless runs), no libretro input/audio, m_current is sampled by the
frontend while GS may already be rendering the next frame (single
buffered), no OSD.
* Libretro: M3 frame pacing, joypad input, audio
- Pacing: PublishFrame now blocks the GS thread until retro_run consumes
the frame (one presented frame per retro_run; the frontend's cadence is
the emulation's vsync). Enabled at context_reset, aborted before any
path that could otherwise leave the GS thread parked (context_destroy,
retro_unload_game, VKLibretro::Shutdown). Static screens still dupe
thanks to yaps2's SkipDuplicateFrames -- the VM keeps 100% speed and the
frontend gets video_cb(NULL) for unchanged frames.
- Input: retro_run forwards the libretro joypad + both analogs straight
into the DualShock2 bind slots (Pad::GetPad(0)->Set), bypassing
InputManager; SDL input source stays disabled.
- Audio: AudioStream grows a public PullFrames() (frontend-driven pull
from the ring), SPU2 exposes GetOutputStream(), the config pins the
Null backend (mix into the ring, no device thread), and retro_run
drains the ring into audio_batch_cb with float->s16 conversion.
- Config: EmuFolders::Settings is now set explicitly (the libretro path
bypasses SetDataDirectory, so the INI used to land in the cwd).
Verified on Adreno 618 with GT3: internal fps 59.9, speed 100%, frames
publish on change, INI persists under <system>/pcsx2/inis.
* Libretro: Stable backbuffer ring + dynamic PAL av_info
Fixes the RetroArch heap-corruption crash ~2 minutes in (GT3 FMV -> demo
race transition): the published frame was the pooled m_current texture,
which GSDeviceVK recycles while the frontend still samples the view for
cached-frame replays. Frames are now copied into a dedicated ring of
three backbuffer textures owned outside the pool; on a resolution change
the displaced buffer is retired (kept alive until device teardown)
instead of destroyed, because the frontend can replay the old image
indefinitely (e.g. while its menu is open). Verified: headless harness
clean over 4+ minutes at ~100% speed, RetroArch session stable past the
previous crash point.
Also: when the booted VM reports a vertical frequency different from the
NTSC default (PAL 50Hz, progressive), retro_run pushes an updated
retro_system_av_info to the frontend.
* Libretro: M4 save states (retro_serialize/retro_unserialize)
SaveState grows in-memory zip variants sharing the existing disk code:
SaveState_ZipToBuffer writes the ArchiveEntryList into a libzip
buffer source (zip_source_keep + read-back after close), and
SaveState_UnzipFromBuffer opens one over the incoming blob; the whole
entry/version/screenshot pipeline is reused via a shared
SaveState_UnzipFromZip body, so the on-disk and in-memory formats are
identical (a retro state is a valid .p2s payload).
retro_serialize runs SaveState_DownloadState + ZipToBuffer as a blocking
RunOnCPUThread job with frame pacing temporarily disabled -- while the
frontend is inside retro_serialize it is not calling retro_run, so a
parked PublishFrame would deadlock the GS freeze. The fixed
retro_serialize_size bound is 68 MiB (DownloadState's 64 MiB working
buffer + slack); the actual zip length travels as a leading u64 inside
the block.
Verified on GT3 (Adreno 618): serialize 587 ms, unserialize 131 ms,
emulation continues cleanly after the in-place load.
* Libretro: M4 core options + disk control (m3u multi-disc)
Core options (RETRO_ENVIRONMENT_SET_VARIABLES): GS renderer
(Vulkan/Software; the software renderer still presents through the
shared Vulkan context, so the negotiation path is unchanged), internal
resolution 1x-4x (EmuCore/GS upscale_multiplier, live), fast boot and
widescreen patches. Startup values apply before LoadStartupSettings;
later changes re-apply via VMManager::ApplySettings on the CPU thread.
Disk control (SET_DISK_CONTROL_EXT_INTERFACE): .m3u playlists parse
into a disc list (relative entries resolved against the playlist dir),
single-disc content registers as a one-entry list, and closing the tray
swaps via VMManager::ChangeDisc on the CPU thread. m3u added to
valid_extensions.
Regression-tested on GT3: boot, savestate roundtrip and rendering
unchanged.
* Libretro: OSD via real present path into the backbuffer
The surfaceless BeginPresent no longer copies m_current and skips the
frame -- with the libretro context active it begins an actual present
render pass targeting the dedicated backbuffer (clear + viewport/scissor,
same sequence as the swapchain path) and returns PresentResult::OK. The
whole standard presentation pipeline now runs unchanged: PresentRect
draws the display aspect-corrected with TV shaders/linear filtering,
FullscreenUI::Render and ImGuiManager::RenderOSD draw the overlay, and
EndPresent (libretro branch) finishes the backbuffer, submits without
swapchain semantics and publishes the image to the frontend. The old
copy-based publish is gone.
New core option: yaps2_show_fps (EmuCore/GS OsdShowFPS).
Verified on GT3/Adreno 618: readback shows the ImGui FPS counter drawn
over the aspect-corrected frame; boot/savestate regression clean.
* Libretro: Fix retro_serialize bounds check to include the u64 length header
The check compared buffer.size() against the caller's buffer size, but the
write is sizeof(u64) + buffer.size() — a state within 8 bytes of the
reported serialize size would overflow the frontend's buffer.
* Libretro: Size the output canvas to the internal resolution
The present backbuffer was sized from the fixed 640x448 window info, so
the upscale option rendered internally at 2x-4x and then got scaled back
down before the frontend ever saw the frame.
The present path now tracks the merged frame: expand it to the target
aspect ratio (the internal-resolution screenshot rule), clamp it to the
advertised max geometry (2732x2048, 4x PAL at 4:3), and resize the
surfaceless "window" before the draw rect is computed so the whole
frame stays consistent. ResizeWindow learns to adopt a new size with no
swap chain, and retro_run reports geometry changes with SET_GEOMETRY so
the frontend keeps scaling correctly. Resizes are rare in practice (boot,
FMV/interlace switches, option changes) and reuse the existing
retire-don't-destroy backbuffer ring.
* Libretro: Core options v2 + video/performance options
Registers options through SET_CORE_OPTIONS_V2 with Video/Performance/
System categories and per-option help text (legacy SET_VARIABLES kept as
the fallback), and adds:
- Aspect ratio (Auto 4:3/3:2, 4:3, 16:9, Stretch) — the canvas sizing
follows it, and 16:9 pairs with the widescreen patches option
- Deinterlacing mode (Automatic/Off/Weave/Bob/Blend/Adaptive)
- No-interlacing patches (progressive output for supported games)
- Blending accuracy (Minimum-Maximum)
- EE cycle rate (50%-300%) and EE cycle skip speed hacks
* Libretro: Don't double-load the Vulkan library in EnumerateGPUs
The frontend preloads libvulkan for the context negotiation, and
EnumerateGPUs asserted (and would have unloaded the host's library) when
called with no device open — which is exactly the Software renderer
path, via D3D::GetPreferredRenderer. Use the already-loaded library and
leave it loaded.
* Libretro: Second wave of core options
Video: texture filtering, anisotropic filtering, software renderer
threads. Performance: hardware download mode (readbacks are expensive
on tile-based mobile GPUs), MTVU and Instant VU1 toggles. System: BIOS
selection (scanned from <system>/pcsx2/bios at option registration,
auto = first valid image) and cheats (.pnach loading).
* Libretro: Fix crash on content close
Two shutdown bugs, both hit on every quit-from-menu:
- GSDeviceVK::Destroy destroyed the negotiated VkDevice, but that device
belongs to the frontend (it made the vkCreateDevice call) and the
frontend tears it down after context_destroy — RetroArch was left
waiting on and destroying a dead device (freeze, then segfault).
Guard it like the adopted VkInstance already was.
- The frontend replays the last set_image indefinitely (menu background,
duped frames), so retro_unload_game now retracts the image and waits
for the GPU before VM teardown destroys the textures it points at.
Verified: 6/6 clean RetroArch exits after a full Vulkan content run
(was a reliable SIGSEGV on close before).
* Libretro: Reclaim retired presentation backbuffers
A resolution change retires the displaced backbuffer instead of freeing
it, because the frontend may still be replaying its image for a few
cached/duped frames. But the retired list was only cleared at device
teardown, so every interlace<->progressive switch (frequent in FMV-heavy
games) leaked a full-resolution render target for the rest of the
session.
Each retired backbuffer is now tagged with a monotonic present count and
reclaimed once kLibretroRetireFrames (6) presents have gone by --
comfortably beyond any libretro frontend's swapchain depth (2-3), and
GSTextureVK destruction is itself fence-deferred, so the underlying
Vulkan objects aren't freed until the GPU is done with them either.
Verified: GT3 boots/renders through its 640x448<->640x480 interlace
switches with no crash (harness + real RetroArch, clean exit).
* Libretro: Move libretro headers to 3rdparty/libretro
The libretro Vulkan HW-render interface header was pulled in by adding
pcsx2-libretro/ to PCSX2_FLAGS's INTERFACE include path, which leaked
onto every target that consumes PCSX2_FLAGS (and had the GS backend
reaching into the frontend's source dir).
Move libretro.h + libretro_vulkan.h into 3rdparty/libretro/ behind a
header-only INTERFACE library (libretro-headers), and link it PRIVATE
to the two targets that actually need it: PCSX2 (for VKLibretro.cpp) and
the pcsx2-libretro frontend. No other target sees the headers now.
1359 lines
39 KiB
C++
1359 lines
39 KiB
C++
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
|
|
// SPDX-License-Identifier: GPL-3.0+
|
|
|
|
#include "Achievements.h"
|
|
#include "BuildVersion.h"
|
|
#include "CDVD/CDVD.h"
|
|
#include "COP0.h"
|
|
#include "Cache.h"
|
|
#include "Config.h"
|
|
#include "Counters.h"
|
|
#include "DebugTools/Breakpoints.h"
|
|
#include "DebugTools/SymbolImporter.h"
|
|
#include "Elfheader.h"
|
|
#include "GS.h"
|
|
#include "GS/GS.h"
|
|
#include "Host.h"
|
|
#include "MTGS.h"
|
|
#include "MTVU.h"
|
|
#include "Patch.h"
|
|
#include "R3000A.h"
|
|
#include "SIO/Multitap/MultitapProtocol.h"
|
|
#include "SIO/Pad/Pad.h"
|
|
#include "SIO/Sio.h"
|
|
#include "SIO/Sio0.h"
|
|
#include "SIO/Sio2.h"
|
|
#include "SPU2/spu2.h"
|
|
#include "SaveState.h"
|
|
#include "StateWrapper.h"
|
|
#include "USB/USB.h"
|
|
#include "VMManager.h"
|
|
#include "VUmicro.h"
|
|
#include "ps2/BiosTools.h"
|
|
|
|
#include "common/Error.h"
|
|
#include "common/FileSystem.h"
|
|
#include "common/Path.h"
|
|
#include "common/ScopedGuard.h"
|
|
#include "common/StringUtil.h"
|
|
#include "common/ZipHelpers.h"
|
|
|
|
#include "IconsFontAwesome.h"
|
|
#include "fmt/format.h"
|
|
|
|
#include <csetjmp>
|
|
#include <png.h>
|
|
|
|
using namespace R5900;
|
|
|
|
static tlbs s_tlb_backup[std::size(tlb)];
|
|
|
|
static void PreLoadPrep()
|
|
{
|
|
// ensure everything is in sync before we start overwriting stuff.
|
|
if (THREAD_VU1)
|
|
vu1Thread.WaitVU();
|
|
MTGS::WaitGS(false);
|
|
|
|
// backup current TLBs, since we're going to overwrite them all
|
|
std::memcpy(s_tlb_backup, tlb, sizeof(s_tlb_backup));
|
|
|
|
// clear protected pages, since we don't want to fault loading EE memory
|
|
mmap_ResetBlockTracking();
|
|
|
|
VMManager::Internal::ClearCPUExecutionCaches();
|
|
}
|
|
|
|
static void PostLoadPrep()
|
|
{
|
|
resetCache();
|
|
// WriteCP0Status(cpuRegs.CP0.n.Status.val);
|
|
for (int i = 0; i < 48; i++)
|
|
{
|
|
if (std::memcmp(&s_tlb_backup[i], &tlb[i], sizeof(tlbs)) != 0)
|
|
{
|
|
UnmapTLB(s_tlb_backup[i], i);
|
|
MapTLB(tlb[i], i);
|
|
}
|
|
}
|
|
|
|
if (EmuConfig.Gamefixes.GoemonTlbHack) GoemonPreloadTlb();
|
|
CBreakPoints::SetSkipFirst(BREAKPOINT_EE, 0);
|
|
CBreakPoints::SetSkipFirst(BREAKPOINT_IOP, 0);
|
|
|
|
UpdateVSyncRate(true);
|
|
|
|
if (VMManager::Internal::HasBootedELF())
|
|
R5900SymbolImporter.OnElfLoadedInMemory();
|
|
}
|
|
|
|
// --------------------------------------------------------------------------------------
|
|
// SaveStateBase (implementations)
|
|
// --------------------------------------------------------------------------------------
|
|
SaveStateBase::SaveStateBase(VmStateBuffer& memblock)
|
|
: m_memory(memblock)
|
|
, m_version(g_SaveVersion)
|
|
{
|
|
}
|
|
|
|
void SaveStateBase::PrepBlock(int size)
|
|
{
|
|
if (m_error)
|
|
return;
|
|
|
|
const int end = m_idx + size;
|
|
if (IsSaving())
|
|
{
|
|
if (static_cast<u32>(end) >= m_memory.size())
|
|
m_memory.resize(static_cast<u32>(end));
|
|
}
|
|
else
|
|
{
|
|
if (m_memory.size() < static_cast<u32>(end))
|
|
{
|
|
Console.Error("(SaveStateBase) Buffer overflow in PrepBlock(), expected %d got %zu", end, m_memory.size());
|
|
m_error = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool SaveStateBase::FreezeTag(const char* src)
|
|
{
|
|
if (m_error)
|
|
return false;
|
|
|
|
char tagspace[32];
|
|
pxAssertMsg(std::strlen(src) < (sizeof(tagspace) - 1), "Tag name exceeds the allowed length");
|
|
|
|
std::memset(tagspace, 0, sizeof(tagspace));
|
|
StringUtil::Strlcpy(tagspace, src, sizeof(tagspace));
|
|
Freeze(tagspace);
|
|
|
|
if (std::strcmp(tagspace, src) != 0)
|
|
{
|
|
Console.Error(fmt::format("Savestate data corruption detected while reading tag: {}", src));
|
|
m_error = true;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SaveStateBase::FreezeBios()
|
|
{
|
|
if (!FreezeTag("BIOS"))
|
|
return false;
|
|
|
|
// Check the BIOS, and issue a warning if the bios for this state
|
|
// doesn't match the bios currently being used (chances are it'll still
|
|
// work fine, but some games are very picky).
|
|
|
|
u32 bioscheck = BiosChecksum;
|
|
char biosdesc[256];
|
|
std::memset(biosdesc, 0, sizeof(biosdesc));
|
|
StringUtil::Strlcpy(biosdesc, BiosDescription, sizeof(biosdesc));
|
|
|
|
Freeze(bioscheck);
|
|
Freeze(biosdesc);
|
|
|
|
if (bioscheck != BiosChecksum)
|
|
{
|
|
Console.Error("\n Warning: BIOS Version Mismatch, savestate may be unstable!");
|
|
Console.Error(
|
|
" Current BIOS: %s (crc=0x%08x)\n"
|
|
" Savestate BIOS: %s (crc=0x%08x)\n",
|
|
BiosDescription.c_str(), BiosChecksum,
|
|
biosdesc, bioscheck
|
|
);
|
|
}
|
|
|
|
return IsOkay();
|
|
}
|
|
|
|
bool SaveStateBase::FreezeInternals(Error* error)
|
|
{
|
|
// Print this until the MTVU problem in gifPathFreeze is taken care of (rama)
|
|
if (THREAD_VU1)
|
|
Console.Warning("MTVU speedhack is enabled, saved states may not be stable");
|
|
|
|
if (!vmFreeze())
|
|
return false;
|
|
|
|
// Second Block - Various CPU Registers and States
|
|
// -----------------------------------------------
|
|
if (!FreezeTag("cpuRegs"))
|
|
return false;
|
|
|
|
Freeze(cpuRegs); // cpu regs + COP0
|
|
Freeze(psxRegs); // iop regs
|
|
Freeze(fpuRegs);
|
|
Freeze(tlb); // tlbs
|
|
Freeze(cachedTlbs); // cached tlbs
|
|
Freeze(AllowParams1); //OSDConfig written (Fast Boot)
|
|
Freeze(AllowParams2);
|
|
|
|
// Third Block - Cycle Timers and Events
|
|
// -------------------------------------
|
|
if (!FreezeTag("Cycles"))
|
|
return false;
|
|
|
|
Freeze(EEsCycle);
|
|
Freeze(EEoCycle);
|
|
Freeze(nextDeltaCounter);
|
|
Freeze(nextStartCounter);
|
|
Freeze(psxNextStartCounter);
|
|
Freeze(psxNextDeltaCounter);
|
|
|
|
// Fourth Block - EE-related systems
|
|
// ---------------------------------
|
|
if (!FreezeTag("EE-Subsystems"))
|
|
return false;
|
|
|
|
bool okay = rcntFreeze();
|
|
okay = okay && memFreeze(error);
|
|
okay = okay && gsFreeze();
|
|
okay = okay && vuMicroFreeze();
|
|
okay = okay && vuJITFreeze();
|
|
okay = okay && vif0Freeze();
|
|
okay = okay && vif1Freeze();
|
|
okay = okay && sifFreeze();
|
|
okay = okay && ipuFreeze();
|
|
okay = okay && ipuDmaFreeze();
|
|
okay = okay && gifFreeze();
|
|
okay = okay && gifDmaFreeze();
|
|
okay = okay && sprFreeze();
|
|
okay = okay && mtvuFreeze();
|
|
if (!okay)
|
|
return false;
|
|
|
|
// Fifth Block - iop-related systems
|
|
// ---------------------------------
|
|
if (!FreezeTag("IOP-Subsystems"))
|
|
return false;
|
|
|
|
FreezeMem(iopMem->Sif, sizeof(iopMem->Sif)); // iop's sif memory (not really needed, but oh well)
|
|
|
|
okay = okay && psxRcntFreeze();
|
|
|
|
// TODO: move all the others over to StateWrapper too...
|
|
if (!okay)
|
|
return false;
|
|
{
|
|
// This is horrible. We need to move the rest over...
|
|
std::optional<StateWrapper::VectorMemoryStream> save_stream;
|
|
std::optional<StateWrapper::ReadOnlyMemoryStream> load_stream;
|
|
if (IsSaving())
|
|
save_stream.emplace();
|
|
else
|
|
load_stream.emplace(&m_memory[m_idx], static_cast<int>(m_memory.size()) - m_idx);
|
|
|
|
StateWrapper sw(IsSaving() ? static_cast<StateWrapper::IStream*>(&save_stream.value()) :
|
|
static_cast<StateWrapper::IStream*>(&load_stream.value()),
|
|
IsSaving() ? StateWrapper::Mode::Write : StateWrapper::Mode::Read, g_SaveVersion);
|
|
|
|
okay = okay && g_Sio0.DoState(sw);
|
|
okay = okay && g_Sio2.DoState(sw);
|
|
okay = okay && g_MultitapArr.at(0).DoState(sw);
|
|
okay = okay && g_MultitapArr.at(1).DoState(sw);
|
|
|
|
if (!okay || !sw.IsGood())
|
|
return false;
|
|
|
|
if (IsSaving())
|
|
{
|
|
FreezeMem(const_cast<u8*>(save_stream->GetBuffer().data()), save_stream->GetPosition());
|
|
}
|
|
else
|
|
{
|
|
const int new_idx = m_idx + static_cast<int>(load_stream->GetPosition());
|
|
if (static_cast<size_t>(new_idx) >= m_memory.size())
|
|
return false;
|
|
|
|
m_idx = new_idx;
|
|
}
|
|
}
|
|
|
|
okay = okay && cdrFreeze();
|
|
okay = okay && cdvdFreeze();
|
|
|
|
// technically this is HLE BIOS territory, but we don't have enough such stuff
|
|
// to merit an HLE Bios sub-section... yet.
|
|
okay = okay && deci2Freeze();
|
|
|
|
okay = okay && InputRecordingFreeze();
|
|
|
|
okay = okay && handleFreeze(); //file handles
|
|
|
|
return okay;
|
|
}
|
|
|
|
|
|
// --------------------------------------------------------------------------------------
|
|
// memSavingState (implementations)
|
|
// --------------------------------------------------------------------------------------
|
|
// uncompressed to/from memory state saves implementation
|
|
|
|
memSavingState::memSavingState(VmStateBuffer& save_to)
|
|
: SaveStateBase(save_to)
|
|
{
|
|
}
|
|
|
|
// Saving of state data
|
|
void memSavingState::FreezeMem(void* data, int size)
|
|
{
|
|
if (!size) return;
|
|
|
|
const int new_size = m_idx + size;
|
|
if (static_cast<u32>(new_size) > m_memory.size())
|
|
m_memory.resize(static_cast<u32>(new_size));
|
|
|
|
std::memcpy(&m_memory[m_idx], data, size);
|
|
m_idx += size;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------------------
|
|
// memLoadingState (implementations)
|
|
// --------------------------------------------------------------------------------------
|
|
memLoadingState::memLoadingState(const VmStateBuffer& load_from)
|
|
: SaveStateBase(const_cast<VmStateBuffer&>(load_from))
|
|
{
|
|
}
|
|
|
|
// Loading of state data from a memory buffer...
|
|
void memLoadingState::FreezeMem( void* data, int size )
|
|
{
|
|
if (static_cast<u32>(m_idx + size) > m_memory.size())
|
|
m_error = true;
|
|
|
|
if (m_error)
|
|
{
|
|
std::memset(data, 0, size);
|
|
return;
|
|
}
|
|
|
|
const u8* const src = &m_memory[m_idx];
|
|
m_idx += size;
|
|
std::memcpy(data, src, size);
|
|
}
|
|
|
|
static const char* EntryFilename_StateVersion = "PCSX2 Savestate Version.id";
|
|
static const char* EntryFilename_Screenshot = "Screenshot.png";
|
|
static const char* EntryFilename_InternalStructures = "PCSX2 Internal Structures.dat";
|
|
static constexpr u32 STATE_PCSX2_VERSION_SIZE = 32;
|
|
|
|
struct SysState_Component
|
|
{
|
|
const char* name;
|
|
int (*freeze)(FreezeAction, freezeData*);
|
|
};
|
|
|
|
static int SysState_MTGSFreeze(FreezeAction mode, freezeData* fP)
|
|
{
|
|
MTGS::FreezeData sstate = { fP, 0 };
|
|
MTGS::Freeze(mode, sstate);
|
|
return sstate.retval;
|
|
}
|
|
|
|
static constexpr SysState_Component SPU2_{ "SPU2", SPU2freeze };
|
|
static constexpr SysState_Component GS{ "GS", SysState_MTGSFreeze };
|
|
|
|
static bool SysState_ComponentFreezeIn(zip_file_t* zf, SysState_Component comp)
|
|
{
|
|
if (!zf)
|
|
return true;
|
|
|
|
freezeData fP = { 0, nullptr };
|
|
if (comp.freeze(FreezeAction::Size, &fP) != 0)
|
|
fP.size = 0;
|
|
|
|
Console.WriteLn(" Loading %s", comp.name);
|
|
|
|
std::unique_ptr<u8[]> data;
|
|
if (fP.size > 0)
|
|
{
|
|
data = std::make_unique<u8[]>(fP.size);
|
|
fP.data = data.get();
|
|
|
|
if (zip_fread(zf, data.get(), fP.size) != static_cast<zip_int64_t>(fP.size))
|
|
{
|
|
Console.Error(fmt::format("* {}: Failed to decompress save data", comp.name));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (comp.freeze(FreezeAction::Load, &fP) != 0)
|
|
{
|
|
Console.Error(fmt::format("* {}: Failed to load freeze data", comp.name));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool SysState_ComponentFreezeOut(SaveStateBase& writer, SysState_Component comp)
|
|
{
|
|
freezeData fP = {};
|
|
if (comp.freeze(FreezeAction::Size, &fP) != 0)
|
|
{
|
|
Console.Error(fmt::format("* {}: Failed to get freeze size", comp.name));
|
|
return false;
|
|
}
|
|
|
|
if (fP.size == 0)
|
|
return true;
|
|
|
|
const int size = fP.size;
|
|
writer.PrepBlock(size);
|
|
|
|
Console.WriteLn(" Saving %s", comp.name);
|
|
|
|
fP.data = writer.GetBlockPtr();
|
|
if (comp.freeze(FreezeAction::Save, &fP) != 0)
|
|
{
|
|
Console.Error(fmt::format("* {}: Failed to save freeze data", comp.name));
|
|
return false;
|
|
}
|
|
|
|
writer.CommitBlock(size);
|
|
return true;
|
|
}
|
|
|
|
static bool SysState_ComponentFreezeInNew(zip_file_t* zf, const char* name, bool(*do_state_func)(StateWrapper&))
|
|
{
|
|
// TODO: We could decompress on the fly here for a little bit more speed.
|
|
std::vector<u8> data;
|
|
if (zf)
|
|
{
|
|
std::optional<std::vector<u8>> optdata(ReadBinaryFileInZip(zf));
|
|
if (optdata.has_value())
|
|
data = std::move(optdata.value());
|
|
}
|
|
|
|
StateWrapper::ReadOnlyMemoryStream stream(data.empty() ? nullptr : data.data(), data.size());
|
|
StateWrapper sw(&stream, StateWrapper::Mode::Read, g_SaveVersion);
|
|
|
|
return do_state_func(sw);
|
|
}
|
|
|
|
static bool SysState_ComponentFreezeOutNew(SaveStateBase& writer, const char* name, u32 reserve, bool (*do_state_func)(StateWrapper&))
|
|
{
|
|
StateWrapper::VectorMemoryStream stream(reserve);
|
|
StateWrapper sw(&stream, StateWrapper::Mode::Write, g_SaveVersion);
|
|
|
|
if (!do_state_func(sw))
|
|
return false;
|
|
|
|
const int size = static_cast<int>(stream.GetBuffer().size());
|
|
if (size > 0)
|
|
{
|
|
writer.PrepBlock(size);
|
|
std::memcpy(writer.GetBlockPtr(), stream.GetBuffer().data(), size);
|
|
writer.CommitBlock(size);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------------------
|
|
// BaseSavestateEntry
|
|
// --------------------------------------------------------------------------------------
|
|
class BaseSavestateEntry
|
|
{
|
|
protected:
|
|
BaseSavestateEntry() = default;
|
|
|
|
public:
|
|
virtual ~BaseSavestateEntry() = default;
|
|
|
|
virtual const char* GetFilename() const = 0;
|
|
virtual bool FreezeIn(zip_file_t* zf) const = 0;
|
|
virtual bool FreezeOut(SaveStateBase& writer) const = 0;
|
|
virtual bool IsRequired() const = 0;
|
|
};
|
|
|
|
class MemorySavestateEntry : public BaseSavestateEntry
|
|
{
|
|
protected:
|
|
MemorySavestateEntry() {}
|
|
virtual ~MemorySavestateEntry() = default;
|
|
|
|
public:
|
|
virtual bool FreezeIn(zip_file_t* zf) const;
|
|
virtual bool FreezeOut(SaveStateBase& writer) const;
|
|
virtual bool IsRequired() const { return true; }
|
|
|
|
protected:
|
|
virtual u8* GetDataPtr() const = 0;
|
|
virtual u32 GetDataSize() const = 0;
|
|
};
|
|
|
|
bool MemorySavestateEntry::FreezeIn(zip_file_t* zf) const
|
|
{
|
|
const u32 expectedSize = GetDataSize();
|
|
const s64 bytesRead = zip_fread(zf, GetDataPtr(), expectedSize);
|
|
if (bytesRead != static_cast<s64>(expectedSize))
|
|
{
|
|
Console.WriteLn(Color_Yellow, " '%s' is incomplete (expected 0x%x bytes, loading only 0x%x bytes)",
|
|
GetFilename(), expectedSize, static_cast<u32>(bytesRead));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool MemorySavestateEntry::FreezeOut(SaveStateBase& writer) const
|
|
{
|
|
writer.FreezeMem(GetDataPtr(), GetDataSize());
|
|
return writer.IsOkay();
|
|
}
|
|
|
|
// --------------------------------------------------------------------------------------
|
|
// SavestateEntry_* (EmotionMemory, IopMemory, etc)
|
|
// --------------------------------------------------------------------------------------
|
|
// Implementation Rationale:
|
|
// The address locations of PS2 virtual memory components is fully dynamic, so we need to
|
|
// resolve the pointers at the time they are requested (eeMem, iopMem, etc). Thusly, we
|
|
// cannot use static struct member initializers -- we need virtual functions that compute
|
|
// and resolve the addresses on-demand instead... --air
|
|
|
|
class SavestateEntry_EmotionMemory final : public MemorySavestateEntry
|
|
{
|
|
public:
|
|
~SavestateEntry_EmotionMemory() override = default;
|
|
|
|
const char* GetFilename() const override { return "eeMemory.bin"; }
|
|
u8* GetDataPtr() const override { return eeMem->Main; }
|
|
uint GetDataSize() const override { return Ps2MemSize::ExposedRam; }
|
|
|
|
virtual bool FreezeIn(zip_file_t* zf) const override
|
|
{
|
|
return MemorySavestateEntry::FreezeIn(zf);
|
|
}
|
|
};
|
|
|
|
class SavestateEntry_IopMemory final : public MemorySavestateEntry
|
|
{
|
|
public:
|
|
~SavestateEntry_IopMemory() override = default;
|
|
|
|
const char* GetFilename() const override { return "iopMemory.bin"; }
|
|
u8* GetDataPtr() const override { return iopMem->Main; }
|
|
uint GetDataSize() const override { return Ps2MemSize::ExposedIopRam; }
|
|
};
|
|
|
|
class SavestateEntry_HwRegs final : public MemorySavestateEntry
|
|
{
|
|
public:
|
|
~SavestateEntry_HwRegs() override = default;
|
|
|
|
const char* GetFilename() const override { return "eeHwRegs.bin"; }
|
|
u8* GetDataPtr() const override { return eeHw; }
|
|
uint GetDataSize() const override { return sizeof(eeHw); }
|
|
};
|
|
|
|
class SavestateEntry_IopHwRegs final : public MemorySavestateEntry
|
|
{
|
|
public:
|
|
~SavestateEntry_IopHwRegs() = default;
|
|
|
|
const char* GetFilename() const override { return "iopHwRegs.bin"; }
|
|
u8* GetDataPtr() const override { return iopHw; }
|
|
uint GetDataSize() const override { return sizeof(iopHw); }
|
|
};
|
|
|
|
class SavestateEntry_Scratchpad final : public MemorySavestateEntry
|
|
{
|
|
public:
|
|
~SavestateEntry_Scratchpad() = default;
|
|
|
|
const char* GetFilename() const override { return "Scratchpad.bin"; }
|
|
u8* GetDataPtr() const override { return eeMem->Scratch; }
|
|
uint GetDataSize() const override { return sizeof(eeMem->Scratch); }
|
|
};
|
|
|
|
class SavestateEntry_VU0mem final : public MemorySavestateEntry
|
|
{
|
|
public:
|
|
~SavestateEntry_VU0mem() = default;
|
|
|
|
const char* GetFilename() const override { return "vu0Memory.bin"; }
|
|
u8* GetDataPtr() const override { return vuRegs[0].Mem; }
|
|
uint GetDataSize() const override { return VU0_MEMSIZE; }
|
|
};
|
|
|
|
class SavestateEntry_VU1mem final : public MemorySavestateEntry
|
|
{
|
|
public:
|
|
~SavestateEntry_VU1mem() = default;
|
|
|
|
const char* GetFilename() const override { return "vu1Memory.bin"; }
|
|
u8* GetDataPtr() const override { return vuRegs[1].Mem; }
|
|
uint GetDataSize() const override { return VU1_MEMSIZE; }
|
|
};
|
|
|
|
class SavestateEntry_VU0prog final : public MemorySavestateEntry
|
|
{
|
|
public:
|
|
~SavestateEntry_VU0prog() = default;
|
|
|
|
const char* GetFilename() const override { return "vu0MicroMem.bin"; }
|
|
u8* GetDataPtr() const override { return vuRegs[0].Micro; }
|
|
uint GetDataSize() const override { return VU0_PROGSIZE; }
|
|
};
|
|
|
|
class SavestateEntry_VU1prog final : public MemorySavestateEntry
|
|
{
|
|
public:
|
|
~SavestateEntry_VU1prog() = default;
|
|
|
|
const char* GetFilename() const override { return "vu1MicroMem.bin"; }
|
|
u8* GetDataPtr() const override { return vuRegs[1].Micro; }
|
|
uint GetDataSize() const override { return VU1_PROGSIZE; }
|
|
};
|
|
|
|
class SavestateEntry_SPU2 final : public BaseSavestateEntry
|
|
{
|
|
public:
|
|
~SavestateEntry_SPU2() override = default;
|
|
|
|
const char* GetFilename() const override { return "SPU2.bin"; }
|
|
bool FreezeIn(zip_file_t* zf) const override { return SysState_ComponentFreezeIn(zf, SPU2_); }
|
|
bool FreezeOut(SaveStateBase& writer) const override { return SysState_ComponentFreezeOut(writer, SPU2_); }
|
|
bool IsRequired() const override { return true; }
|
|
};
|
|
|
|
class SavestateEntry_USB final : public BaseSavestateEntry
|
|
{
|
|
public:
|
|
~SavestateEntry_USB() override = default;
|
|
|
|
const char* GetFilename() const override { return "USB.bin"; }
|
|
bool FreezeIn(zip_file_t* zf) const override { return SysState_ComponentFreezeInNew(zf, "USB", &USB::DoState); }
|
|
bool FreezeOut(SaveStateBase& writer) const override { return SysState_ComponentFreezeOutNew(writer, "USB", 16 * 1024, &USB::DoState); }
|
|
bool IsRequired() const override { return false; }
|
|
};
|
|
|
|
class SavestateEntry_PAD final : public BaseSavestateEntry
|
|
{
|
|
public:
|
|
~SavestateEntry_PAD() override = default;
|
|
|
|
const char* GetFilename() const override { return "PAD.bin"; }
|
|
bool FreezeIn(zip_file_t* zf) const override { return SysState_ComponentFreezeInNew(zf, "PAD", &Pad::Freeze); }
|
|
bool FreezeOut(SaveStateBase& writer) const override { return SysState_ComponentFreezeOutNew(writer, "PAD", 16 * 1024, &Pad::Freeze); }
|
|
bool IsRequired() const override { return true; }
|
|
};
|
|
|
|
class SavestateEntry_GS final : public BaseSavestateEntry
|
|
{
|
|
public:
|
|
~SavestateEntry_GS() = default;
|
|
|
|
const char* GetFilename() const { return "GS.bin"; }
|
|
bool FreezeIn(zip_file_t* zf) const { return SysState_ComponentFreezeIn(zf, GS); }
|
|
bool FreezeOut(SaveStateBase& writer) const { return SysState_ComponentFreezeOut(writer, GS); }
|
|
bool IsRequired() const { return true; }
|
|
};
|
|
|
|
class SaveStateEntry_Achievements final : public BaseSavestateEntry
|
|
{
|
|
~SaveStateEntry_Achievements() override = default;
|
|
|
|
const char* GetFilename() const override { return "Achievements.bin"; }
|
|
bool FreezeIn(zip_file_t* zf) const override
|
|
{
|
|
if (!Achievements::IsActive())
|
|
return true;
|
|
|
|
std::optional<std::vector<u8>> data;
|
|
if (zf)
|
|
data = ReadBinaryFileInZip(zf);
|
|
|
|
if (data.has_value())
|
|
Achievements::LoadState(data.value());
|
|
else
|
|
Achievements::LoadState(std::span<const u8>());
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FreezeOut(SaveStateBase& writer) const override
|
|
{
|
|
if (!Achievements::IsActive())
|
|
return true;
|
|
|
|
Achievements::SaveState(writer);
|
|
return writer.IsOkay();
|
|
}
|
|
|
|
bool IsRequired() const override { return false; }
|
|
};
|
|
|
|
// (cpuRegs, iopRegs, VPU/GIF/DMAC structures should all remain as part of a larger unified
|
|
// block, since they're all PCSX2-dependent and having separate files in the archie for them
|
|
// would not be useful).
|
|
//
|
|
|
|
static const std::unique_ptr<BaseSavestateEntry> SavestateEntries[] = {
|
|
std::unique_ptr<BaseSavestateEntry>(new SavestateEntry_EmotionMemory),
|
|
std::unique_ptr<BaseSavestateEntry>(new SavestateEntry_IopMemory),
|
|
std::unique_ptr<BaseSavestateEntry>(new SavestateEntry_HwRegs),
|
|
std::unique_ptr<BaseSavestateEntry>(new SavestateEntry_IopHwRegs),
|
|
std::unique_ptr<BaseSavestateEntry>(new SavestateEntry_Scratchpad),
|
|
std::unique_ptr<BaseSavestateEntry>(new SavestateEntry_VU0mem),
|
|
std::unique_ptr<BaseSavestateEntry>(new SavestateEntry_VU1mem),
|
|
std::unique_ptr<BaseSavestateEntry>(new SavestateEntry_VU0prog),
|
|
std::unique_ptr<BaseSavestateEntry>(new SavestateEntry_VU1prog),
|
|
std::unique_ptr<BaseSavestateEntry>(new SavestateEntry_SPU2),
|
|
std::unique_ptr<BaseSavestateEntry>(new SavestateEntry_USB),
|
|
std::unique_ptr<BaseSavestateEntry>(new SavestateEntry_PAD),
|
|
std::unique_ptr<BaseSavestateEntry>(new SavestateEntry_GS),
|
|
std::unique_ptr<BaseSavestateEntry>(new SaveStateEntry_Achievements),
|
|
};
|
|
|
|
std::unique_ptr<ArchiveEntryList> SaveState_DownloadState(Error* error)
|
|
{
|
|
std::unique_ptr<ArchiveEntryList> destlist = std::make_unique<ArchiveEntryList>();
|
|
destlist->GetBuffer().resize(1024 * 1024 * 64);
|
|
|
|
memSavingState saveme(destlist->GetBuffer());
|
|
ArchiveEntry internals(EntryFilename_InternalStructures);
|
|
internals.SetDataIndex(saveme.GetCurrentPos());
|
|
|
|
if (!saveme.FreezeBios())
|
|
{
|
|
Error::SetString(error, "FreezeBios() failed");
|
|
return nullptr;
|
|
}
|
|
|
|
if (!saveme.FreezeInternals(error))
|
|
{
|
|
if (!error->IsValid())
|
|
Error::SetString(error, "FreezeInternals() failed");
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
internals.SetDataSize(saveme.GetCurrentPos() - internals.GetDataIndex());
|
|
destlist->Add(internals);
|
|
|
|
for (const std::unique_ptr<BaseSavestateEntry>& entry : SavestateEntries)
|
|
{
|
|
uint startpos = saveme.GetCurrentPos();
|
|
if (!entry->FreezeOut(saveme))
|
|
{
|
|
Error::SetString(error, fmt::format("FreezeOut() failed for {}.", entry->GetFilename()));
|
|
destlist.reset();
|
|
break;
|
|
}
|
|
|
|
destlist->Add(
|
|
ArchiveEntry(entry->GetFilename())
|
|
.SetDataIndex(startpos)
|
|
.SetDataSize(saveme.GetCurrentPos() - startpos));
|
|
}
|
|
|
|
return destlist;
|
|
}
|
|
|
|
std::unique_ptr<SaveStateScreenshotData> SaveState_SaveScreenshot()
|
|
{
|
|
static constexpr u32 SCREENSHOT_WIDTH = 640;
|
|
static constexpr u32 SCREENSHOT_HEIGHT = 480;
|
|
|
|
u32 width, height;
|
|
std::vector<u32> pixels;
|
|
if (!MTGS::SaveMemorySnapshot(SCREENSHOT_WIDTH, SCREENSHOT_HEIGHT, true, false, &width, &height, &pixels))
|
|
{
|
|
// saving failed for some reason, device lost?
|
|
return nullptr;
|
|
}
|
|
|
|
std::unique_ptr<SaveStateScreenshotData> data = std::make_unique<SaveStateScreenshotData>();
|
|
data->width = width;
|
|
data->height = height;
|
|
data->pixels = std::move(pixels);
|
|
return data;
|
|
}
|
|
|
|
static bool SaveState_CompressScreenshot(SaveStateScreenshotData* data, zip_t* zf)
|
|
{
|
|
zip_error_t ze = {};
|
|
zip_source_t* const zs = zip_source_buffer_create(nullptr, 0, 0, &ze);
|
|
if (!zs)
|
|
return false;
|
|
|
|
if (zip_source_begin_write(zs) != 0)
|
|
{
|
|
zip_source_free(zs);
|
|
return false;
|
|
}
|
|
|
|
ScopedGuard zs_free([zs]() { zip_source_free(zs); });
|
|
|
|
png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
|
|
png_infop info_ptr = nullptr;
|
|
if (!png_ptr)
|
|
return false;
|
|
|
|
ScopedGuard cleanup([&png_ptr, &info_ptr]() {
|
|
if (png_ptr)
|
|
png_destroy_write_struct(&png_ptr, info_ptr ? &info_ptr : nullptr);
|
|
});
|
|
|
|
info_ptr = png_create_info_struct(png_ptr);
|
|
if (!info_ptr)
|
|
return false;
|
|
|
|
if (setjmp(png_jmpbuf(png_ptr)))
|
|
return false;
|
|
|
|
png_set_write_fn(png_ptr, zs, [](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
|
|
zip_source_write(static_cast<zip_source_t*>(png_get_io_ptr(png_ptr)), data_ptr, size);
|
|
}, [](png_structp png_ptr) {});
|
|
png_set_compression_level(png_ptr, 5);
|
|
png_set_IHDR(png_ptr, info_ptr, data->width, data->height, 8, PNG_COLOR_TYPE_RGBA,
|
|
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
|
|
png_write_info(png_ptr, info_ptr);
|
|
|
|
for (u32 y = 0; y < data->height; ++y)
|
|
{
|
|
// ensure the alpha channel is set to opaque
|
|
u32* row = &data->pixels[y * data->width];
|
|
for (u32 x = 0; x < data->width; x++)
|
|
row[x] |= 0xFF000000u;
|
|
|
|
png_write_row(png_ptr, reinterpret_cast<png_bytep>(row));
|
|
}
|
|
|
|
png_write_end(png_ptr, nullptr);
|
|
|
|
if (zip_source_commit_write(zs) != 0)
|
|
return false;
|
|
|
|
const s64 file_index = zip_file_add(zf, EntryFilename_Screenshot, zs, 0);
|
|
if (file_index < 0)
|
|
return false;
|
|
|
|
// png is already compressed, no point doing it twice
|
|
zip_set_file_compression(zf, file_index, ZIP_CM_STORE, 0);
|
|
|
|
// source is now owned by the zip file for later compression
|
|
zs_free.Cancel();
|
|
return true;
|
|
}
|
|
|
|
static bool SaveState_ReadScreenshot(zip_t* zf, u32* out_width, u32* out_height, std::vector<u32>* out_pixels)
|
|
{
|
|
auto zff = zip_fopen_managed(zf, EntryFilename_Screenshot, 0);
|
|
if (!zff)
|
|
return false;
|
|
|
|
png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
|
|
if (!png_ptr)
|
|
return false;
|
|
|
|
png_infop info_ptr = png_create_info_struct(png_ptr);
|
|
if (!info_ptr)
|
|
{
|
|
png_destroy_read_struct(&png_ptr, nullptr, nullptr);
|
|
return false;
|
|
}
|
|
|
|
ScopedGuard cleanup([&png_ptr, &info_ptr]() {
|
|
png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
|
|
});
|
|
|
|
if (setjmp(png_jmpbuf(png_ptr)))
|
|
return false;
|
|
|
|
png_set_read_fn(png_ptr, zff.get(), [](png_structp png_ptr, png_bytep data_ptr, png_size_t size) {
|
|
zip_fread(static_cast<zip_file_t*>(png_get_io_ptr(png_ptr)), data_ptr, size);
|
|
});
|
|
|
|
png_read_info(png_ptr, info_ptr);
|
|
|
|
png_uint_32 width = 0;
|
|
png_uint_32 height = 0;
|
|
int bitDepth = 0;
|
|
int colorType = -1;
|
|
if (png_get_IHDR(png_ptr, info_ptr, &width, &height, &bitDepth, &colorType, nullptr, nullptr, nullptr) != 1 ||
|
|
width == 0 || height == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const png_uint_32 bytesPerRow = png_get_rowbytes(png_ptr, info_ptr);
|
|
std::vector<u8> rowData(bytesPerRow);
|
|
|
|
*out_width = width;
|
|
*out_height = height;
|
|
out_pixels->resize(width * height);
|
|
|
|
for (u32 y = 0; y < height; y++)
|
|
{
|
|
png_read_row(png_ptr, static_cast<png_bytep>(rowData.data()), nullptr);
|
|
|
|
const u8* row_ptr = rowData.data();
|
|
u32* out_ptr = &out_pixels->at(y * width);
|
|
if (colorType == PNG_COLOR_TYPE_RGB)
|
|
{
|
|
for (u32 x = 0; x < width; x++)
|
|
{
|
|
u32 pixel = static_cast<u32>(*(row_ptr)++);
|
|
pixel |= static_cast<u32>(*(row_ptr)++) << 8;
|
|
pixel |= static_cast<u32>(*(row_ptr)++) << 16;
|
|
pixel |= static_cast<u32>(*(row_ptr)++) << 24;
|
|
*(out_ptr++) = pixel | 0xFF000000u; // make opaque
|
|
}
|
|
}
|
|
else if (colorType == PNG_COLOR_TYPE_RGBA)
|
|
{
|
|
for (u32 x = 0; x < width; x++)
|
|
{
|
|
u32 pixel;
|
|
std::memcpy(&pixel, row_ptr, sizeof(u32));
|
|
row_ptr += sizeof(u32);
|
|
*(out_ptr++) = pixel | 0xFF000000u; // make opaque
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// --------------------------------------------------------------------------------------
|
|
// CompressThread_VmState
|
|
// --------------------------------------------------------------------------------------
|
|
static bool SaveState_AddToZip(zip_t* zf, ArchiveEntryList* srclist, SaveStateScreenshotData* screenshot)
|
|
{
|
|
u32 compression = ZIP_CM_DEFAULT;
|
|
u32 compression_level = 0;
|
|
|
|
if (EmuConfig.Savestate.CompressionType == SavestateCompressionMethod::Zstandard)
|
|
{
|
|
compression = ZIP_CM_ZSTD;
|
|
|
|
if (EmuConfig.Savestate.CompressionRatio == SavestateCompressionLevel::Low)
|
|
compression_level = 1;
|
|
else if (EmuConfig.Savestate.CompressionRatio == SavestateCompressionLevel::Medium)
|
|
compression_level = 3;
|
|
else if (EmuConfig.Savestate.CompressionRatio == SavestateCompressionLevel::High)
|
|
compression_level = 10;
|
|
else if (EmuConfig.Savestate.CompressionRatio == SavestateCompressionLevel::VeryHigh)
|
|
compression_level = 22;
|
|
}
|
|
else if (EmuConfig.Savestate.CompressionType == SavestateCompressionMethod::Deflate)
|
|
{
|
|
compression = ZIP_CM_DEFLATE;
|
|
if (EmuConfig.Savestate.CompressionRatio == SavestateCompressionLevel::Low)
|
|
compression_level = 1;
|
|
else if (EmuConfig.Savestate.CompressionRatio == SavestateCompressionLevel::Medium)
|
|
compression_level = 5;
|
|
else if (EmuConfig.Savestate.CompressionRatio == SavestateCompressionLevel::High)
|
|
compression_level = 7;
|
|
else if (EmuConfig.Savestate.CompressionRatio == SavestateCompressionLevel::VeryHigh)
|
|
compression_level = 9;
|
|
}
|
|
else if (EmuConfig.Savestate.CompressionType == SavestateCompressionMethod::Uncompressed)
|
|
{
|
|
compression = ZIP_CM_STORE;
|
|
compression_level = 0;
|
|
}
|
|
|
|
// version indicator
|
|
{
|
|
struct VersionIndicator
|
|
{
|
|
u32 save_version;
|
|
char version[STATE_PCSX2_VERSION_SIZE];
|
|
};
|
|
|
|
VersionIndicator* vi = static_cast<VersionIndicator*>(std::malloc(sizeof(VersionIndicator)));
|
|
vi->save_version = g_SaveVersion;
|
|
if (BuildVersion::GitTaggedCommit)
|
|
{
|
|
StringUtil::Strlcpy(vi->version, BuildVersion::GitTag, std::size(vi->version));
|
|
}
|
|
else
|
|
{
|
|
StringUtil::Strlcpy(vi->version, "Unknown", std::size(vi->version));
|
|
}
|
|
|
|
zip_source_t* const zs = zip_source_buffer(zf, vi, sizeof(*vi), 1);
|
|
if (!zs)
|
|
{
|
|
std::free(vi);
|
|
return false;
|
|
}
|
|
|
|
// NOTE: Source should not be freed if successful.
|
|
const s64 fi = zip_file_add(zf, EntryFilename_StateVersion, zs, ZIP_FL_ENC_UTF_8);
|
|
if (fi < 0)
|
|
{
|
|
zip_source_free(zs);
|
|
return false;
|
|
}
|
|
|
|
// Don't compress the version indicator file so that builds that don't
|
|
// support a given compression method can at least still read it.
|
|
zip_set_file_compression(zf, fi, ZIP_CM_STORE, 0);
|
|
}
|
|
|
|
const uint listlen = srclist->GetLength();
|
|
for (uint i = 0; i < listlen; ++i)
|
|
{
|
|
const ArchiveEntry& entry = (*srclist)[i];
|
|
if (!entry.GetDataSize())
|
|
continue;
|
|
|
|
zip_source_t* const zs = zip_source_buffer(zf, srclist->GetPtr(entry.GetDataIndex()), entry.GetDataSize(), 0);
|
|
if (!zs)
|
|
return false;
|
|
|
|
const s64 fi = zip_file_add(zf, entry.GetFilename().c_str(), zs, ZIP_FL_ENC_UTF_8);
|
|
if (fi < 0)
|
|
{
|
|
zip_source_free(zs);
|
|
return false;
|
|
}
|
|
|
|
zip_set_file_compression(zf, fi, compression, compression_level);
|
|
}
|
|
|
|
if (screenshot)
|
|
{
|
|
if (!SaveState_CompressScreenshot(screenshot, zf))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool SaveState_ZipToDisk(
|
|
std::unique_ptr<ArchiveEntryList> srclist, std::unique_ptr<SaveStateScreenshotData> screenshot,
|
|
const char* filename, Error* error)
|
|
{
|
|
zip_error_t ze = {};
|
|
zip_source_t* zs = zip_source_file_create(filename, 0, 0, &ze);
|
|
zip_t* zf = nullptr;
|
|
if (zs && !(zf = zip_open_from_source(zs, ZIP_CREATE | ZIP_TRUNCATE, &ze)))
|
|
{
|
|
Error::SetStringFmt(error,
|
|
TRANSLATE_FS("SaveState", "Failed to open zip file '{}' for save state: {}."),
|
|
filename, zip_error_strerror(&ze));
|
|
|
|
// have to clean up source
|
|
zip_source_free(zs);
|
|
return false;
|
|
}
|
|
|
|
// discard zip file if we fail saving something
|
|
if (!SaveState_AddToZip(zf, srclist.get(), screenshot.get()))
|
|
{
|
|
Error::SetStringFmt(error,
|
|
TRANSLATE_FS("SaveState", "Failed to save state to zip file '{}'."), filename);
|
|
zip_discard(zf);
|
|
return false;
|
|
}
|
|
|
|
// force the zip to close, this is the expensive part with libzip.
|
|
if (zip_close(zf) != 0)
|
|
{
|
|
Error::SetStringFmt(error,
|
|
TRANSLATE_FS("SaveState", "Failed to save state to zip file '{}': {}."), filename, zip_strerror(zf));
|
|
zip_discard(zf);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// libretro: write a state into an in-memory zip (retro_serialize buffer).
|
|
bool SaveState_ZipToBuffer(std::unique_ptr<ArchiveEntryList> srclist,
|
|
std::unique_ptr<SaveStateScreenshotData> screenshot, std::vector<u8>* out_data, Error* error)
|
|
{
|
|
zip_error_t ze = {};
|
|
zip_source_t* zs = zip_source_buffer_create(nullptr, 0, 0, &ze);
|
|
zip_t* zf = nullptr;
|
|
if (zs && !(zf = zip_open_from_source(zs, ZIP_TRUNCATE, &ze)))
|
|
{
|
|
Error::SetString(error, fmt::format("Savestate buffer zip error: {}", zip_error_strerror(&ze)));
|
|
zip_source_free(zs);
|
|
return false;
|
|
}
|
|
|
|
// keep the source alive across zip_close so the bytes can be read back
|
|
zip_source_keep(zs);
|
|
|
|
if (!SaveState_AddToZip(zf, srclist.get(), screenshot.get()))
|
|
{
|
|
Error::SetString(error, "Failed to save state to memory zip.");
|
|
zip_discard(zf);
|
|
zip_source_free(zs);
|
|
return false;
|
|
}
|
|
|
|
if (zip_close(zf) != 0)
|
|
{
|
|
Error::SetString(error, fmt::format("Failed to close memory zip: {}", zip_strerror(zf)));
|
|
zip_discard(zf);
|
|
zip_source_free(zs);
|
|
return false;
|
|
}
|
|
|
|
bool res = false;
|
|
if (zip_source_open(zs) == 0)
|
|
{
|
|
if (zip_source_seek(zs, 0, SEEK_END) == 0)
|
|
{
|
|
const zip_int64_t size = zip_source_tell(zs);
|
|
if (size > 0 && zip_source_seek(zs, 0, SEEK_SET) == 0)
|
|
{
|
|
out_data->resize(static_cast<size_t>(size));
|
|
res = (zip_source_read(zs, out_data->data(), out_data->size()) ==
|
|
static_cast<zip_int64_t>(out_data->size()));
|
|
}
|
|
}
|
|
zip_source_close(zs);
|
|
}
|
|
if (!res)
|
|
Error::SetString(error, "Failed to read back memory zip.");
|
|
|
|
zip_source_free(zs);
|
|
return res;
|
|
}
|
|
|
|
bool SaveState_ReadScreenshot(const std::string& filename, u32* out_width, u32* out_height, std::vector<u32>* out_pixels)
|
|
{
|
|
zip_error_t ze = {};
|
|
auto zf = zip_open_managed(filename.c_str(), ZIP_RDONLY, &ze);
|
|
if (!zf)
|
|
{
|
|
Console.Error("Failed to open zip file '%s' for save state screenshot: %s", filename.c_str(), zip_error_strerror(&ze));
|
|
return false;
|
|
}
|
|
|
|
return SaveState_ReadScreenshot(zf.get(), out_width, out_height, out_pixels);
|
|
}
|
|
|
|
static bool CheckVersion(const std::string& filename, zip_t* zf, Error* error)
|
|
{
|
|
u32 savever;
|
|
|
|
auto zff = zip_fopen_managed(zf, EntryFilename_StateVersion, 0);
|
|
if (!zff || zip_fread(zff.get(), &savever, sizeof(savever)) != sizeof(savever))
|
|
{
|
|
Error::SetString(error, "Savestate file does not contain version indicator.");
|
|
return false;
|
|
}
|
|
|
|
char version_string[STATE_PCSX2_VERSION_SIZE];
|
|
if (zip_fread(zff.get(), version_string, STATE_PCSX2_VERSION_SIZE) == STATE_PCSX2_VERSION_SIZE)
|
|
version_string[STATE_PCSX2_VERSION_SIZE - 1] = 0;
|
|
else
|
|
StringUtil::Strlcpy(version_string, "Unknown", std::size(version_string));
|
|
|
|
// Major version mismatch. Means we can't load this savestate at all. Support for it
|
|
// was removed entirely.
|
|
// check for a "minor" version incompatibility; which happens if the savestate being loaded is a newer version
|
|
// than the emulator recognizes. 99% chance that trying to load it will just corrupt emulation or crash.
|
|
if (savever > g_SaveVersion || (savever >> 16) != (g_SaveVersion >> 16))
|
|
{
|
|
std::string current_emulator_version = BuildVersion::GitTag;
|
|
if (current_emulator_version.empty())
|
|
{
|
|
current_emulator_version = "Unknown";
|
|
}
|
|
Error::SetString(error, fmt::format(TRANSLATE_FS("SaveState","This save state was created with PCSX2 version {0}. It is no longer compatible "
|
|
"with your current PCSX2 version {1}.\n\n"
|
|
"If you have any unsaved progress on this save state, you can download the compatible PCSX2 version {0} "
|
|
"from pcsx2.net, load the save state, and save your progress to the memory card."),
|
|
version_string, current_emulator_version));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static zip_int64_t CheckFileExistsInState(zip_t* zf, const char* name, bool required)
|
|
{
|
|
zip_int64_t index = zip_name_locate(zf, name, /*ZIP_FL_NOCASE*/ 0);
|
|
if (index >= 0)
|
|
{
|
|
DevCon.WriteLn(Color_Green, " ... found '%s'", name);
|
|
return index;
|
|
}
|
|
|
|
if (required)
|
|
Console.WriteLn(Color_Red, " ... not found '%s'!", name);
|
|
else
|
|
DevCon.WriteLn(Color_Red, " ... not found '%s'!", name);
|
|
|
|
return index;
|
|
}
|
|
|
|
static bool LoadInternalStructuresState(zip_t* zf, s64 index, Error* error)
|
|
{
|
|
zip_stat_t zst;
|
|
if (zip_stat_index(zf, index, 0, &zst) != 0 || zst.size > std::numeric_limits<int>::max())
|
|
return false;
|
|
|
|
// Load all the internal data
|
|
auto zff = zip_fopen_index_managed(zf, index, 0);
|
|
if (!zff)
|
|
return false;
|
|
|
|
std::vector<u8> buffer(zst.size);
|
|
if (zip_fread(zff.get(), buffer.data(), buffer.size()) != static_cast<zip_int64_t>(buffer.size()))
|
|
return false;
|
|
|
|
memLoadingState state(buffer);
|
|
if (!state.FreezeBios())
|
|
return false;
|
|
|
|
if (!state.FreezeInternals(error))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool SaveState_UnzipFromZip(zip_t* zf, const std::string& label, Error* error);
|
|
|
|
bool SaveState_UnzipFromDisk(const std::string& filename, Error* error)
|
|
{
|
|
zip_error_t ze = {};
|
|
auto zf = zip_open_managed(filename.c_str(), ZIP_RDONLY, &ze);
|
|
if (!zf)
|
|
{
|
|
Console.Error("Failed to open zip file '%s' for save state load: %s", filename.c_str(), zip_error_strerror(&ze));
|
|
if (zip_error_code_zip(&ze) == ZIP_ER_NOENT)
|
|
Error::SetString(error, "Savestate file does not exist.");
|
|
else
|
|
Error::SetString(error, fmt::format("Savestate zip error: {}", zip_error_strerror(&ze)));
|
|
|
|
return false;
|
|
}
|
|
|
|
return SaveState_UnzipFromZip(zf.get(), filename, error);
|
|
}
|
|
|
|
// libretro: load a state from an in-memory zip (retro_unserialize buffer).
|
|
bool SaveState_UnzipFromBuffer(const void* data, size_t size, Error* error)
|
|
{
|
|
zip_error_t ze = {};
|
|
zip_source_t* zs = zip_source_buffer_create(data, size, 0, &ze);
|
|
zip_t* zf_raw = zs ? zip_open_from_source(zs, ZIP_RDONLY, &ze) : nullptr;
|
|
if (!zf_raw)
|
|
{
|
|
Error::SetString(error, fmt::format("Savestate buffer zip error: {}", zip_error_strerror(&ze)));
|
|
if (zs)
|
|
zip_source_free(zs);
|
|
return false;
|
|
}
|
|
|
|
const bool res = SaveState_UnzipFromZip(zf_raw, "<memory>", error);
|
|
zip_discard(zf_raw);
|
|
return res;
|
|
}
|
|
|
|
static bool SaveState_UnzipFromZip(zip_t* zf_raw, const std::string& filename, Error* error)
|
|
{
|
|
// Wrap in the same interface the file path uses.
|
|
struct ZipRef
|
|
{
|
|
zip_t* p;
|
|
zip_t* get() const { return p; }
|
|
} zf{zf_raw};
|
|
|
|
// look for version and screenshot information in the zip stream:
|
|
if (!CheckVersion(filename, zf.get(), error))
|
|
return false;
|
|
|
|
// check that all parts are included
|
|
const s64 internal_index = CheckFileExistsInState(zf.get(), EntryFilename_InternalStructures, true);
|
|
s64 entryIndices[std::size(SavestateEntries)];
|
|
|
|
// Log any parts and pieces that are missing, and then generate an exception.
|
|
bool allPresent = (internal_index >= 0);
|
|
for (u32 i = 0; i < std::size(SavestateEntries); i++)
|
|
{
|
|
const bool required = SavestateEntries[i]->IsRequired();
|
|
entryIndices[i] = CheckFileExistsInState(zf.get(), SavestateEntries[i]->GetFilename(), required);
|
|
if (entryIndices[i] < 0 && required)
|
|
{
|
|
allPresent = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!allPresent)
|
|
{
|
|
Error::SetString(error, "Some required components were not found or are incomplete.");
|
|
return false;
|
|
}
|
|
|
|
PreLoadPrep();
|
|
|
|
if (!LoadInternalStructuresState(zf.get(), internal_index, error))
|
|
{
|
|
if (!error->IsValid())
|
|
Error::SetString(error, "Save state corruption in internal structures.");
|
|
|
|
VMManager::Reset();
|
|
return false;
|
|
}
|
|
|
|
for (u32 i = 0; i < std::size(SavestateEntries); ++i)
|
|
{
|
|
if (entryIndices[i] < 0)
|
|
{
|
|
SavestateEntries[i]->FreezeIn(nullptr);
|
|
continue;
|
|
}
|
|
|
|
auto zff = zip_fopen_index_managed(zf.get(), entryIndices[i], 0);
|
|
if (!zff || !SavestateEntries[i]->FreezeIn(zff.get()))
|
|
{
|
|
Error::SetString(error, fmt::format("Save state corruption in {}.", SavestateEntries[i]->GetFilename()));
|
|
VMManager::Reset();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
PostLoadPrep();
|
|
return true;
|
|
}
|
|
|
|
void SaveState_ReportLoadErrorOSD(const std::string& message, std::optional<s32> slot, bool backup)
|
|
{
|
|
std::string full_message;
|
|
if (slot.has_value())
|
|
{
|
|
if (backup)
|
|
full_message = fmt::format(
|
|
TRANSLATE_FS("SaveState", "Failed to load state from backup slot {}: {}"), *slot, message);
|
|
else
|
|
full_message = fmt::format(
|
|
TRANSLATE_FS("SaveState", "Failed to load state from slot {}: {}"), *slot, message);
|
|
}
|
|
else
|
|
{
|
|
full_message = fmt::format(TRANSLATE_FS("SaveState", "Failed to load state: {}"), message);
|
|
}
|
|
|
|
Host::AddIconOSDMessage("LoadState", ICON_FA_TRIANGLE_EXCLAMATION,
|
|
full_message, Host::OSD_WARNING_DURATION);
|
|
}
|
|
|
|
void SaveState_ReportSaveErrorOSD(const std::string& message, std::optional<s32> slot)
|
|
{
|
|
std::string full_message;
|
|
if (slot.has_value())
|
|
full_message = fmt::format(
|
|
TRANSLATE_FS("SaveState", "Failed to save state to slot {}: {}"), *slot, message);
|
|
else
|
|
full_message = fmt::format(TRANSLATE_FS("SaveState", "Failed to save state: {}"), message);
|
|
|
|
Host::AddIconOSDMessage("SaveState", ICON_FA_TRIANGLE_EXCLAMATION,
|
|
full_message, Host::OSD_WARNING_DURATION);
|
|
}
|