diff --git a/CMakeLists.txt b/CMakeLists.txt index 0b5c329c9b..288f463b6b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -735,6 +735,8 @@ elseif(IOS) ios/main.mm ios/AppDelegate.mm ios/AppDelegate.h + ios/DisplayManager.h + ios/DisplayManager.mm ios/ViewController.mm ios/ViewController.h ios/iOSCoreAudio.mm @@ -1379,8 +1381,11 @@ set(GPU_SOURCES GPU/Debugger/Breakpoints.h GPU/Debugger/Debugger.cpp GPU/Debugger/Debugger.h + GPU/Debugger/Playback.cpp + GPU/Debugger/Playback.h GPU/Debugger/Record.cpp GPU/Debugger/Record.h + GPU/Debugger/RecordFormat.h GPU/Debugger/Stepping.cpp GPU/Debugger/Stepping.h GPU/GPUInterface.h @@ -1987,6 +1992,7 @@ if(TargetBin) endif() if(IOS) + list(APPEND NativeAssets ios/Settings.bundle) add_executable(${TargetBin} MACOSX_BUNDLE ${ICON_PATH_ABS} ${NativeAssets} ${SHADER_FILES} ${FLASH0_FILES} ${LANG_FILES} ${NativeAppSource} "ios/Launch Screen.storyboard") else() add_executable(${TargetBin} MACOSX_BUNDLE ${ICON_PATH_ABS} ${NativeAssets} ${SHADER_FILES} ${FLASH0_FILES} ${LANG_FILES} ${NativeAppSource}) @@ -2043,6 +2049,7 @@ if(IOS) set_target_properties(${TargetBin} PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/ios/PPSSPP-Info.plist" RESOURCE "ios/Launch Screen.storyboard" + RESOURCE "ios/Settings.bundle" XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET ${DEPLOYMENT_TARGET} XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "iPhone/iPad" XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC YES diff --git a/Common/ChunkFile.cpp b/Common/ChunkFile.cpp index 4e2334e735..22327a7575 100644 --- a/Common/ChunkFile.cpp +++ b/Common/ChunkFile.cpp @@ -249,19 +249,27 @@ CChunkFileReader::Error CChunkFileReader::LoadFile(const std::string &filename, return ERROR_BAD_FILE; } - _buffer = buffer; if (header.Compress) { u8 *uncomp_buffer = new u8[header.UncompressedSize]; size_t uncomp_size = header.UncompressedSize; - snappy_uncompress((const char *)buffer, sz, (char *)uncomp_buffer, &uncomp_size); + auto status = snappy_uncompress((const char *)buffer, sz, (char *)uncomp_buffer, &uncomp_size); + if (status != SNAPPY_OK) { + ERROR_LOG(SAVESTATE, "ChunkReader: Failed to decompress file"); + delete [] uncomp_buffer; + delete [] buffer; + return ERROR_BAD_FILE; + } if ((u32)uncomp_size != header.UncompressedSize) { ERROR_LOG(SAVESTATE, "Size mismatch: file: %u calc: %u", header.UncompressedSize, (u32)uncomp_size); delete [] uncomp_buffer; + delete [] buffer; return ERROR_BAD_FILE; } _buffer = uncomp_buffer; sz = uncomp_size; delete [] buffer; + } else { + _buffer = buffer; } if (header.GitVersion[31]) { diff --git a/Common/Vulkan/VulkanContext.cpp b/Common/Vulkan/VulkanContext.cpp index 732351aa43..76c67ff9ed 100644 --- a/Common/Vulkan/VulkanContext.cpp +++ b/Common/Vulkan/VulkanContext.cpp @@ -105,6 +105,11 @@ VkResult VulkanContext::CreateInstance(const CreateInfo &info) { return VK_ERROR_INITIALIZATION_FAILED; } + if (!IsInstanceExtensionAvailable(VK_KHR_SURFACE_EXTENSION_NAME)) { + // Cannot create a Vulkan display without VK_KHR_SURFACE_EXTENSION. + init_error_ = "Vulkan not loaded - no surface extension"; + return VK_ERROR_INITIALIZATION_FAILED; + } flags_ = info.flags; // List extensions to try to enable. diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 36241fece3..8116388cf6 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -58,7 +58,7 @@ #include "Core/HLE/KernelWaitHelpers.h" #include "Core/ELF/ParamSFO.h" -#include "GPU/Debugger/Record.h" +#include "GPU/Debugger/Playback.h" #include "GPU/GPU.h" #include "GPU/GPUInterface.h" #include "GPU/GPUState.h" diff --git a/GPU/Common/GPUStateUtils.cpp b/GPU/Common/GPUStateUtils.cpp index 018664a00d..0002824cf4 100644 --- a/GPU/Common/GPUStateUtils.cpp +++ b/GPU/Common/GPUStateUtils.cpp @@ -1247,6 +1247,10 @@ static void ConvertStencilFunc5551(GenericStencilFuncState &state) { state.testRef = usedRef; // Nuke the mask as well, since this is always/never, just for consistency. state.testMask = 0xFF; + } else { + // Not used, so let's make the ref 0xFF which is a useful value later. + state.testRef = 0xFF; + state.testMask = 0xFF; } }; @@ -1342,6 +1346,10 @@ static void ConvertStencilFunc5551(GenericStencilFuncState &state) { // Otherwise we get 1, which we mostly handle, but won't INVERT correctly. rewriteOps(GE_STENCILOP_INCR, GE_STENCILOP_INVERT); } + if (!usesRef && state.testRef == 0xFF) { + // Safe to use REPLACE instead of INCR. + rewriteOps(GE_STENCILOP_INCR, GE_STENCILOP_REPLACE); + } } void ConvertStencilFuncState(GenericStencilFuncState &state) { diff --git a/GPU/Debugger/Playback.cpp b/GPU/Debugger/Playback.cpp new file mode 100644 index 0000000000..806d43db9f --- /dev/null +++ b/GPU/Debugger/Playback.cpp @@ -0,0 +1,706 @@ +// Copyright (c) 2017- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#include +#include +#include +#include +#include +#include "profiler/profiler.h" +#include "Common/Common.h" +#include "Common/Log.h" +#include "Core/Core.h" +#include "Core/CoreTiming.h" +#include "Core/FileSystems/MetaFileSystem.h" +#include "Core/HLE/sceDisplay.h" +#include "Core/HLE/sceKernelMemory.h" +#include "Core/MemMap.h" +#include "Core/MIPS/MIPS.h" +#include "Core/System.h" +#include "GPU/GPUInterface.h" +#include "GPU/GPUState.h" +#include "GPU/ge_constants.h" +#include "GPU/Debugger/Playback.h" +#include "GPU/Debugger/Record.h" +#include "GPU/Debugger/RecordFormat.h" + +namespace GPURecord { + +static std::string lastExecFilename; +static std::vector lastExecCommands; +static std::vector lastExecPushbuf; + +// This class maps pushbuffer (dump data) sections to PSP memory. +// Dumps can be larger than available PSP memory, because they include generated data too. +// +// If possible, it maps to dynamically allocated "slabs" so nearby access is fast. +// Otherwise it uses "extra" allocations to manage sections that straddle two slabs. +// Slabs are managed with LRU, extra buffers are round-robin. +class BufMapping { +public: + BufMapping(const std::vector &pushbuf) : pushbuf_(pushbuf) { + } + + // Returns a pointer to contiguous memory for this access, or else 0 (failure). + u32 Map(u32 bufpos, u32 sz, const std::function &flush); + + // Clear and reset allocations made. + void Reset() { + slabGeneration_ = 0; + extraOffset_ = 0; + for (int i = 0; i < SLAB_COUNT; ++i) { + slabs_[i].Free(); + } + for (int i = 0; i < EXTRA_COUNT; ++i) { + extra_[i].Free(); + } + } + +protected: + u32 MapSlab(u32 bufpos, const std::function &flush); + u32 MapExtra(u32 bufpos, u32 sz, const std::function &flush); + + enum { + // These numbers kept low because we only have 24 MB of user memory to map into. + SLAB_SIZE = 1 * 1024 * 1024, + // 10 is the number of texture units + verts + inds. + // In the worst case, we could concurrently need 10 slabs/extras at the same time. + SLAB_COUNT = 10, + EXTRA_COUNT = 10, + }; + + // The current "generation". Static simply as a convenience for access. + // This increments on every allocation, for a simple LRU. + static int slabGeneration_; + + // An aligned large mapping of the pushbuffer in PSP RAM. + struct SlabInfo { + u32 psp_pointer_ = 0; + u32 buf_pointer_ = 0; + int last_used_ = 0; + + bool Matches(u32 bufpos) { + // We check psp_pointer_ because bufpos = 0 is valid, and the initial value. + return buf_pointer_ == bufpos && psp_pointer_ != 0; + } + + // Automatically marks used for LRU purposes. + u32 Ptr(u32 bufpos) { + last_used_ = slabGeneration_; + return psp_pointer_ + (bufpos - buf_pointer_); + } + + int Age() const { + // If not allocated, it's as expired as it's gonna get. + if (psp_pointer_ == 0) + return std::numeric_limits::max(); + return slabGeneration_ - last_used_; + } + + bool Alloc(); + void Free(); + bool Setup(u32 bufpos, const std::vector &pushbuf_); + }; + + // An adhoc mapping of the pushbuffer (either larger than a slab or straddling slabs.) + // Remember: texture data, verts, etc. must be contiguous. + struct ExtraInfo { + u32 psp_pointer_ = 0; + u32 buf_pointer_ = 0; + u32 size_ = 0; + + bool Matches(u32 bufpos, u32 sz) { + // We check psp_pointer_ because bufpos = 0 is valid, and the initial value. + return buf_pointer_ == bufpos && psp_pointer_ != 0 && size_ >= sz; + } + + u32 Ptr() { + return psp_pointer_; + } + + bool Alloc(u32 bufpos, u32 sz, const std::vector &pushbuf_); + void Free(); + }; + + SlabInfo slabs_[SLAB_COUNT]{}; + u32 extraOffset_ = 0; + ExtraInfo extra_[EXTRA_COUNT]{}; + + const std::vector &pushbuf_; +}; + +u32 BufMapping::Map(u32 bufpos, u32 sz, const std::function &flush) { + int slab1 = bufpos / SLAB_SIZE; + int slab2 = (bufpos + sz - 1) / SLAB_SIZE; + + if (slab1 == slab2) { + // Doesn't straddle, so we can just map to a slab. + return MapSlab(bufpos, flush); + } else { + // We need contiguous, so we'll just allocate separately. + return MapExtra(bufpos, sz, flush); + } +} + +u32 BufMapping::MapSlab(u32 bufpos, const std::function &flush) { + u32 slab_pos = (bufpos / SLAB_SIZE) * SLAB_SIZE; + + int best = 0; + for (int i = 0; i < SLAB_COUNT; ++i) { + if (slabs_[i].Matches(slab_pos)) { + return slabs_[i].Ptr(bufpos); + } + + if (slabs_[i].Age() > slabs_[best].Age()) { + best = i; + } + } + + // Stall before mapping a new slab. + flush(); + + // Okay, we need to allocate. + if (!slabs_[best].Setup(slab_pos, pushbuf_)) { + return 0; + } + return slabs_[best].Ptr(bufpos); +} + +u32 BufMapping::MapExtra(u32 bufpos, u32 sz, const std::function &flush) { + for (int i = 0; i < EXTRA_COUNT; ++i) { + // Might be likely to reuse larger buffers straddling slabs. + if (extra_[i].Matches(bufpos, sz)) { + return extra_[i].Ptr(); + } + } + + // Stall first, so we don't stomp existing RAM. + flush(); + + int i = extraOffset_; + extraOffset_ = (extraOffset_ + 1) % EXTRA_COUNT; + + if (!extra_[i].Alloc(bufpos, sz, pushbuf_)) { + // Let's try to power on - hopefully none of these are still in use. + for (int i = 0; i < EXTRA_COUNT; ++i) { + extra_[i].Free(); + } + if (!extra_[i].Alloc(bufpos, sz, pushbuf_)) { + return 0; + } + } + return extra_[i].Ptr(); +} + +bool BufMapping::SlabInfo::Alloc() { + u32 sz = SLAB_SIZE; + psp_pointer_ = userMemory.Alloc(sz, false, "Slab"); + if (psp_pointer_ == -1) { + psp_pointer_ = 0; + } + return psp_pointer_ != 0; +} + +void BufMapping::SlabInfo::Free() { + if (psp_pointer_) { + userMemory.Free(psp_pointer_); + psp_pointer_ = 0; + buf_pointer_ = 0; + last_used_ = 0; + } +} + +bool BufMapping::ExtraInfo::Alloc(u32 bufpos, u32 sz, const std::vector &pushbuf_) { + // Make sure we've freed any previous allocation first. + Free(); + + u32 allocSize = sz; + psp_pointer_ = userMemory.Alloc(allocSize, false, "Straddle extra"); + if (psp_pointer_ == -1) { + psp_pointer_ = 0; + } + if (psp_pointer_ == 0) { + return false; + } + + buf_pointer_ = bufpos; + size_ = sz; + Memory::MemcpyUnchecked(psp_pointer_, pushbuf_.data() + bufpos, sz); + return true; +} + +void BufMapping::ExtraInfo::Free() { + if (psp_pointer_) { + userMemory.Free(psp_pointer_); + psp_pointer_ = 0; + buf_pointer_ = 0; + } +} + +bool BufMapping::SlabInfo::Setup(u32 bufpos, const std::vector &pushbuf_) { + // If it already has RAM, we're simply taking it over. Slabs come only in one size. + if (psp_pointer_ == 0) { + if (!Alloc()) { + return false; + } + } + + buf_pointer_ = bufpos; + u32 sz = std::min((u32)SLAB_SIZE, (u32)pushbuf_.size() - bufpos); + Memory::MemcpyUnchecked(psp_pointer_, pushbuf_.data() + bufpos, sz); + + slabGeneration_++; + last_used_ = slabGeneration_; + return true; +} + +int BufMapping::slabGeneration_ = 0; + +class DumpExecute { +public: + DumpExecute(const std::vector &pushbuf, const std::vector &commands) + : pushbuf_(pushbuf), commands_(commands), mapping_(pushbuf) { + } + ~DumpExecute(); + + bool Run(); + +private: + void SyncStall(); + bool SubmitCmds(const void *p, u32 sz); + void SubmitListEnd(); + + void Init(u32 ptr, u32 sz); + void Registers(u32 ptr, u32 sz); + void Vertices(u32 ptr, u32 sz); + void Indices(u32 ptr, u32 sz); + void Clut(u32 ptr, u32 sz); + void TransferSrc(u32 ptr, u32 sz); + void Memset(u32 ptr, u32 sz); + void MemcpyDest(u32 ptr, u32 sz); + void Memcpy(u32 ptr, u32 sz); + void Texture(int level, u32 ptr, u32 sz); + void Framebuf(int level, u32 ptr, u32 sz); + void Display(u32 ptr, u32 sz); + + u32 execMemcpyDest = 0; + u32 execListBuf = 0; + u32 execListPos = 0; + u32 execListID = 0; + const int LIST_BUF_SIZE = 256 * 1024; + std::vector execListQueue; + u16 lastBufw_[8]{}; + + const std::vector &pushbuf_; + const std::vector &commands_; + BufMapping mapping_; +}; + +void DumpExecute::SyncStall() { + gpu->UpdateStall(execListID, execListPos); + s64 listTicks = gpu->GetListTicks(execListID); + if (listTicks != -1) { + s64 nowTicks = CoreTiming::GetTicks(); + if (listTicks > nowTicks) { + currentMIPS->downcount -= listTicks - nowTicks; + } + } + + // Make sure downcount doesn't overflow. + CoreTiming::ForceCheck(); +} + +bool DumpExecute::SubmitCmds(const void *p, u32 sz) { + if (execListBuf == 0) { + u32 allocSize = LIST_BUF_SIZE; + execListBuf = userMemory.Alloc(allocSize, "List buf"); + if (execListBuf == -1) { + execListBuf = 0; + } + if (execListBuf == 0) { + ERROR_LOG(SYSTEM, "Unable to allocate for display list"); + return false; + } + + execListPos = execListBuf; + Memory::Write_U32(GE_CMD_NOP << 24, execListPos); + execListPos += 4; + + gpu->EnableInterrupts(false); + auto optParam = PSPPointer::Create(0); + execListID = gpu->EnqueueList(execListBuf, execListPos, -1, optParam, false); + gpu->EnableInterrupts(true); + } + + u32 pendingSize = (int)execListQueue.size() * sizeof(u32); + // Validate space for jump. + u32 allocSize = pendingSize + sz + 8; + if (execListPos + allocSize >= execListBuf + LIST_BUF_SIZE) { + Memory::Write_U32((GE_CMD_BASE << 24) | ((execListBuf >> 8) & 0x00FF0000), execListPos); + Memory::Write_U32((GE_CMD_JUMP << 24) | (execListBuf & 0x00FFFFFF), execListPos + 4); + + execListPos = execListBuf; + + // Don't continue until we've stalled. + SyncStall(); + } + + Memory::MemcpyUnchecked(execListPos, execListQueue.data(), pendingSize); + execListPos += pendingSize; + u32 writePos = execListPos; + Memory::MemcpyUnchecked(execListPos, p, sz); + execListPos += sz; + + // TODO: Unfortunate. Maybe Texture commands should contain the bufw instead. + // The goal here is to realistically combine prims in dumps. Stalling for the bufw flushes. + u32_le *ops = (u32_le *)Memory::GetPointer(writePos); + for (u32 i = 0; i < sz / 4; ++i) { + u32 cmd = ops[i] >> 24; + if (cmd >= GE_CMD_TEXBUFWIDTH0 && cmd <= GE_CMD_TEXBUFWIDTH7) { + int level = cmd - GE_CMD_TEXBUFWIDTH0; + u16 bufw = ops[i] & 0xFFFF; + + // NOP the address part of the command to avoid a flush too. + if (bufw == lastBufw_[level]) + ops[i] = GE_CMD_NOP << 24; + else + ops[i] = (gstate.texbufwidth[level] & 0xFFFF0000) | bufw; + lastBufw_[level] = bufw; + } + + // Since we're here anyway, also NOP out texture addresses. + // This makes Step Tex not hit phantom textures. + if (cmd >= GE_CMD_TEXADDR0 && cmd <= GE_CMD_TEXADDR7) { + ops[i] = GE_CMD_NOP << 24; + } + } + + execListQueue.clear(); + + return true; +} + +void DumpExecute::SubmitListEnd() { + if (execListPos == 0) { + return; + } + + // There's always space for the end, same size as a jump. + Memory::Write_U32(GE_CMD_FINISH << 24, execListPos); + Memory::Write_U32(GE_CMD_END << 24, execListPos + 4); + execListPos += 8; + + SyncStall(); + gpu->ListSync(execListID, 0); +} + +void DumpExecute::Init(u32 ptr, u32 sz) { + gstate.Restore((u32_le *)(pushbuf_.data() + ptr)); + gpu->ReapplyGfxState(); +} + +void DumpExecute::Registers(u32 ptr, u32 sz) { + SubmitCmds(pushbuf_.data() + ptr, sz); +} + +void DumpExecute::Vertices(u32 ptr, u32 sz) { + u32 psp = mapping_.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this)); + if (psp == 0) { + ERROR_LOG(SYSTEM, "Unable to allocate for vertices"); + return; + } + + execListQueue.push_back((GE_CMD_BASE << 24) | ((psp >> 8) & 0x00FF0000)); + execListQueue.push_back((GE_CMD_VADDR << 24) | (psp & 0x00FFFFFF)); +} + +void DumpExecute::Indices(u32 ptr, u32 sz) { + u32 psp = mapping_.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this)); + if (psp == 0) { + ERROR_LOG(SYSTEM, "Unable to allocate for indices"); + return; + } + + execListQueue.push_back((GE_CMD_BASE << 24) | ((psp >> 8) & 0x00FF0000)); + execListQueue.push_back((GE_CMD_IADDR << 24) | (psp & 0x00FFFFFF)); +} + +void DumpExecute::Clut(u32 ptr, u32 sz) { + u32 psp = mapping_.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this)); + if (psp == 0) { + ERROR_LOG(SYSTEM, "Unable to allocate for clut"); + return; + } + + execListQueue.push_back((GE_CMD_CLUTADDRUPPER << 24) | ((psp >> 8) & 0x00FF0000)); + execListQueue.push_back((GE_CMD_CLUTADDR << 24) | (psp & 0x00FFFFFF)); +} + +void DumpExecute::TransferSrc(u32 ptr, u32 sz) { + u32 psp = mapping_.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this)); + if (psp == 0) { + ERROR_LOG(SYSTEM, "Unable to allocate for transfer"); + return; + } + + // Need to sync in order to access gstate.transfersrcw. + SyncStall(); + + execListQueue.push_back((gstate.transfersrcw & 0xFF00FFFF) | ((psp >> 8) & 0x00FF0000)); + execListQueue.push_back(((GE_CMD_TRANSFERSRC) << 24) | (psp & 0x00FFFFFF)); +} + +void DumpExecute::Memset(u32 ptr, u32 sz) { + PROFILE_THIS_SCOPE("ReplayMemset"); + struct MemsetCommand { + u32 dest; + int value; + u32 sz; + }; + + const MemsetCommand *data = (const MemsetCommand *)(pushbuf_.data() + ptr); + + if (Memory::IsVRAMAddress(data->dest)) { + SyncStall(); + gpu->PerformMemorySet(data->dest, (u8)data->value, data->sz); + } +} + +void DumpExecute::MemcpyDest(u32 ptr, u32 sz) { + execMemcpyDest = *(const u32 *)(pushbuf_.data() + ptr); +} + +void DumpExecute::Memcpy(u32 ptr, u32 sz) { + PROFILE_THIS_SCOPE("ReplayMemcpy"); + if (Memory::IsVRAMAddress(execMemcpyDest)) { + SyncStall(); + Memory::MemcpyUnchecked(execMemcpyDest, pushbuf_.data() + ptr, sz); + gpu->PerformMemoryUpload(execMemcpyDest, sz); + } +} + +void DumpExecute::Texture(int level, u32 ptr, u32 sz) { + u32 psp = mapping_.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this)); + if (psp == 0) { + ERROR_LOG(SYSTEM, "Unable to allocate for texture"); + return; + } + + u32 bufwCmd = GE_CMD_TEXBUFWIDTH0 + level; + u32 addrCmd = GE_CMD_TEXADDR0 + level; + execListQueue.push_back((bufwCmd << 24) | ((psp >> 8) & 0x00FF0000) | lastBufw_[level]); + execListQueue.push_back((addrCmd << 24) | (psp & 0x00FFFFFF)); +} + +void DumpExecute::Framebuf(int level, u32 ptr, u32 sz) { + PROFILE_THIS_SCOPE("ReplayFramebuf"); + struct FramebufData { + u32 addr; + int bufw; + u32 flags; + u32 pad; + }; + + FramebufData *framebuf = (FramebufData *)(pushbuf_.data() + ptr); + + u32 bufwCmd = GE_CMD_TEXBUFWIDTH0 + level; + u32 addrCmd = GE_CMD_TEXADDR0 + level; + execListQueue.push_back((bufwCmd << 24) | ((framebuf->addr >> 8) & 0x00FF0000) | framebuf->bufw); + execListQueue.push_back((addrCmd << 24) | (framebuf->addr & 0x00FFFFFF)); + lastBufw_[level] = framebuf->bufw; + + // And now also copy the data into VRAM (in case it wasn't actually rendered.) + u32 headerSize = (u32)sizeof(FramebufData); + u32 pspSize = sz - headerSize; + const bool isTarget = (framebuf->flags & 1) != 0; + // Could potentially always skip if !isTarget, but playing it safe for offset texture behavior. + if (Memory::IsValidRange(framebuf->addr, pspSize) && (!isTarget || !g_Config.bSoftwareRendering)) { + // Intentionally don't trigger an upload here. + Memory::MemcpyUnchecked(framebuf->addr, pushbuf_.data() + ptr + headerSize, pspSize); + } +} + +void DumpExecute::Display(u32 ptr, u32 sz) { + struct DisplayBufData { + PSPPointer topaddr; + int linesize, pixelFormat; + }; + + DisplayBufData *disp = (DisplayBufData *)(pushbuf_.data() + ptr); + + // Sync up drawing. + SyncStall(); + + __DisplaySetFramebuf(disp->topaddr.ptr, disp->linesize, disp->pixelFormat, 1); + __DisplaySetFramebuf(disp->topaddr.ptr, disp->linesize, disp->pixelFormat, 0); +} + +DumpExecute::~DumpExecute() { + execMemcpyDest = 0; + if (execListBuf) { + userMemory.Free(execListBuf); + execListBuf = 0; + } + execListPos = 0; + mapping_.Reset(); +} + +bool DumpExecute::Run() { + for (const Command &cmd : commands_) { + switch (cmd.type) { + case CommandType::INIT: + Init(cmd.ptr, cmd.sz); + break; + + case CommandType::REGISTERS: + Registers(cmd.ptr, cmd.sz); + break; + + case CommandType::VERTICES: + Vertices(cmd.ptr, cmd.sz); + break; + + case CommandType::INDICES: + Indices(cmd.ptr, cmd.sz); + break; + + case CommandType::CLUT: + Clut(cmd.ptr, cmd.sz); + break; + + case CommandType::TRANSFERSRC: + TransferSrc(cmd.ptr, cmd.sz); + break; + + case CommandType::MEMSET: + Memset(cmd.ptr, cmd.sz); + break; + + case CommandType::MEMCPYDEST: + MemcpyDest(cmd.ptr, cmd.sz); + break; + + case CommandType::MEMCPYDATA: + Memcpy(cmd.ptr, cmd.sz); + break; + + case CommandType::TEXTURE0: + case CommandType::TEXTURE1: + case CommandType::TEXTURE2: + case CommandType::TEXTURE3: + case CommandType::TEXTURE4: + case CommandType::TEXTURE5: + case CommandType::TEXTURE6: + case CommandType::TEXTURE7: + Texture((int)cmd.type - (int)CommandType::TEXTURE0, cmd.ptr, cmd.sz); + break; + + case CommandType::FRAMEBUF0: + case CommandType::FRAMEBUF1: + case CommandType::FRAMEBUF2: + case CommandType::FRAMEBUF3: + case CommandType::FRAMEBUF4: + case CommandType::FRAMEBUF5: + case CommandType::FRAMEBUF6: + case CommandType::FRAMEBUF7: + Framebuf((int)cmd.type - (int)CommandType::FRAMEBUF0, cmd.ptr, cmd.sz); + break; + + case CommandType::DISPLAY: + Display(cmd.ptr, cmd.sz); + break; + + default: + ERROR_LOG(SYSTEM, "Unsupported GE dump command: %d", (int)cmd.type); + return false; + } + } + + SubmitListEnd(); + return true; +} + +static bool ReadCompressed(u32 fp, void *dest, size_t sz) { + u32 compressed_size = 0; + if (pspFileSystem.ReadFile(fp, (u8 *)&compressed_size, sizeof(compressed_size)) != sizeof(compressed_size)) { + return false; + } + + u8 *compressed = new u8[compressed_size]; + if (pspFileSystem.ReadFile(fp, compressed, compressed_size) != compressed_size) { + delete[] compressed; + return false; + } + + size_t real_size = sz; + snappy_uncompress((const char *)compressed, compressed_size, (char *)dest, &real_size); + delete[] compressed; + + return real_size == sz; +} + +static void ReplayStop() { + lastExecFilename.clear(); + lastExecCommands.clear(); + lastExecPushbuf.clear(); +} + +bool RunMountedReplay(const std::string &filename) { + _assert_msg_(SYSTEM, !GPURecord::IsActivePending(), "Cannot run replay while recording."); + + Core_ListenStopRequest(&ReplayStop); + if (lastExecFilename != filename) { + PROFILE_THIS_SCOPE("ReplayLoad"); + u32 fp = pspFileSystem.OpenFile(filename, FILEACCESS_READ); + u8 header[8]{}; + int version = 0; + pspFileSystem.ReadFile(fp, header, sizeof(header)); + pspFileSystem.ReadFile(fp, (u8 *)&version, sizeof(version)); + + if (memcmp(header, HEADER, sizeof(header)) != 0 || version > VERSION || version < MIN_VERSION) { + ERROR_LOG(SYSTEM, "Invalid GE dump or unsupported version"); + pspFileSystem.CloseFile(fp); + return false; + } + + u32 sz = 0; + pspFileSystem.ReadFile(fp, (u8 *)&sz, sizeof(sz)); + u32 bufsz = 0; + pspFileSystem.ReadFile(fp, (u8 *)&bufsz, sizeof(bufsz)); + + lastExecCommands.resize(sz); + lastExecPushbuf.resize(bufsz); + + bool truncated = false; + truncated = truncated || !ReadCompressed(fp, lastExecCommands.data(), sizeof(Command) * sz); + truncated = truncated || !ReadCompressed(fp, lastExecPushbuf.data(), bufsz); + + pspFileSystem.CloseFile(fp); + + if (truncated) { + ERROR_LOG(SYSTEM, "Truncated GE dump"); + return false; + } + + lastExecFilename = filename; + } + + DumpExecute executor(lastExecPushbuf, lastExecCommands); + return executor.Run(); +} + +}; diff --git a/GPU/Debugger/Playback.h b/GPU/Debugger/Playback.h new file mode 100644 index 0000000000..433801cbf2 --- /dev/null +++ b/GPU/Debugger/Playback.h @@ -0,0 +1,26 @@ +// Copyright (c) 2017- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#pragma once + +#include + +namespace GPURecord { + +bool RunMountedReplay(const std::string &filename); + +}; diff --git a/GPU/Debugger/Record.cpp b/GPU/Debugger/Record.cpp index fe25ad1ba6..8d1b216677 100644 --- a/GPU/Debugger/Record.cpp +++ b/GPU/Debugger/Record.cpp @@ -26,13 +26,9 @@ #include "Common/FileUtil.h" #include "Common/Log.h" #include "Core/Core.h" -#include "Core/CoreTiming.h" #include "Core/ELF/ParamSFO.h" -#include "Core/FileSystems/MetaFileSystem.h" #include "Core/HLE/sceDisplay.h" -#include "Core/HLE/sceKernelMemory.h" #include "Core/MemMap.h" -#include "Core/MIPS/MIPS.h" #include "Core/System.h" #include "GPU/GPUInterface.h" #include "GPU/GPUState.h" @@ -40,325 +36,21 @@ #include "GPU/Common/TextureDecoder.h" #include "GPU/Common/VertexDecoderCommon.h" #include "GPU/Debugger/Record.h" +#include "GPU/Debugger/RecordFormat.h" namespace GPURecord { -static const char *HEADER = "PPSSPPGE"; -// Version 1: Uncompressed -// Version 2: Uses snappy -// Version 3: Adds FRAMEBUF0-FRAMEBUF9 -static const int VERSION = 3; -static const int MIN_VERSION = 2; - static bool active = false; static bool nextFrame = false; static int flipLastAction = -1; static std::function writeCallback; -enum class CommandType : u8 { - INIT = 0, - REGISTERS = 1, - VERTICES = 2, - INDICES = 3, - CLUT = 4, - TRANSFERSRC = 5, - MEMSET = 6, - MEMCPYDEST = 7, - MEMCPYDATA = 8, - DISPLAY = 9, - - TEXTURE0 = 0x10, - TEXTURE1 = 0x11, - TEXTURE2 = 0x12, - TEXTURE3 = 0x13, - TEXTURE4 = 0x14, - TEXTURE5 = 0x15, - TEXTURE6 = 0x16, - TEXTURE7 = 0x17, - - FRAMEBUF0 = 0x18, - FRAMEBUF1 = 0x19, - FRAMEBUF2 = 0x1A, - FRAMEBUF3 = 0x1B, - FRAMEBUF4 = 0x1C, - FRAMEBUF5 = 0x1D, - FRAMEBUF6 = 0x1E, - FRAMEBUF7 = 0x1F, -}; - -#pragma pack(push, 1) - -struct Command { - CommandType type; - u32 sz; - u32 ptr; -}; - -#pragma pack(pop) - static std::vector pushbuf; static std::vector commands; static std::vector lastRegisters; static std::vector lastTextures; static std::set lastRenderTargets; -// TODO: Maybe move execute to another file? -class DumpExecute { -public: - ~DumpExecute(); - - bool Run(); - -private: - void SyncStall(); - bool SubmitCmds(void *p, u32 sz); - void SubmitListEnd(); - - void Init(u32 ptr, u32 sz); - void Registers(u32 ptr, u32 sz); - void Vertices(u32 ptr, u32 sz); - void Indices(u32 ptr, u32 sz); - void Clut(u32 ptr, u32 sz); - void TransferSrc(u32 ptr, u32 sz); - void Memset(u32 ptr, u32 sz); - void MemcpyDest(u32 ptr, u32 sz); - void Memcpy(u32 ptr, u32 sz); - void Texture(int level, u32 ptr, u32 sz); - void Framebuf(int level, u32 ptr, u32 sz); - void Display(u32 ptr, u32 sz); - - u32 execMemcpyDest = 0; - u32 execListBuf = 0; - u32 execListPos = 0; - u32 execListID = 0; - const int LIST_BUF_SIZE = 256 * 1024; - std::vector execListQueue; - u16 lastBufw_[8]{}; -}; - -// This class maps pushbuffer (dump data) sections to PSP memory. -// Dumps can be larger than available PSP memory, because they include generated data too. -// -// If possible, it maps to dynamically allocated "slabs" so nearby access is fast. -// Otherwise it uses "extra" allocations to manage sections that straddle two slabs. -// Slabs are managed with LRU, extra buffers are round-robin. -class BufMapping { -public: - // Returns a pointer to contiguous memory for this access, or else 0 (failure). - u32 Map(u32 bufpos, u32 sz, const std::function &flush); - - // Clear and reset allocations made. - void Reset() { - slabGeneration_ = 0; - extraOffset_ = 0; - for (int i = 0; i < SLAB_COUNT; ++i) { - slabs_[i].Free(); - } - for (int i = 0; i < EXTRA_COUNT; ++i) { - extra_[i].Free(); - } - } - -protected: - u32 MapSlab(u32 bufpos, const std::function &flush); - u32 MapExtra(u32 bufpos, u32 sz, const std::function &flush); - - enum { - // These numbers kept low because we only have 24 MB of user memory to map into. - SLAB_SIZE = 1 * 1024 * 1024, - // 10 is the number of texture units + verts + inds. - // In the worst case, we could concurrently need 10 slabs/extras at the same time. - SLAB_COUNT = 10, - EXTRA_COUNT = 10, - }; - - // The current "generation". Static simply as a convenience for access. - // This increments on every allocation, for a simple LRU. - static int slabGeneration_; - - // An aligned large mapping of the pushbuffer in PSP RAM. - struct SlabInfo { - u32 psp_pointer_; - u32 buf_pointer_; - int last_used_; - - bool Matches(u32 bufpos) { - // We check psp_pointer_ because bufpos = 0 is valid, and the initial value. - return buf_pointer_ == bufpos && psp_pointer_ != 0; - } - - // Automatically marks used for LRU purposes. - u32 Ptr(u32 bufpos) { - last_used_ = slabGeneration_; - return psp_pointer_ + (bufpos - buf_pointer_); - } - - int Age() const { - // If not allocated, it's as expired as it's gonna get. - if (psp_pointer_ == 0) - return std::numeric_limits::max(); - return slabGeneration_ - last_used_; - } - - bool Alloc(); - void Free(); - bool Setup(u32 bufpos); - }; - - // An adhoc mapping of the pushbuffer (either larger than a slab or straddling slabs.) - // Remember: texture data, verts, etc. must be contiguous. - struct ExtraInfo { - u32 psp_pointer_; - u32 buf_pointer_; - u32 size_; - - bool Matches(u32 bufpos, u32 sz) { - // We check psp_pointer_ because bufpos = 0 is valid, and the initial value. - return buf_pointer_ == bufpos && psp_pointer_ != 0 && size_ >= sz; - } - - u32 Ptr() { - return psp_pointer_; - } - - bool Alloc(u32 bufpos, u32 sz); - void Free(); - }; - - SlabInfo slabs_[SLAB_COUNT]; - u32 extraOffset_ = 0; - ExtraInfo extra_[EXTRA_COUNT]; -}; - -static BufMapping execMapping; - -u32 BufMapping::Map(u32 bufpos, u32 sz, const std::function &flush) { - int slab1 = bufpos / SLAB_SIZE; - int slab2 = (bufpos + sz - 1) / SLAB_SIZE; - - if (slab1 == slab2) { - // Doesn't straddle, so we can just map to a slab. - return MapSlab(bufpos, flush); - } else { - // We need contiguous, so we'll just allocate separately. - return MapExtra(bufpos, sz, flush); - } -} - -u32 BufMapping::MapSlab(u32 bufpos, const std::function &flush) { - u32 slab_pos = (bufpos / SLAB_SIZE) * SLAB_SIZE; - - int best = 0; - for (int i = 0; i < SLAB_COUNT; ++i) { - if (slabs_[i].Matches(slab_pos)) { - return slabs_[i].Ptr(bufpos); - } - - if (slabs_[i].Age() > slabs_[best].Age()) { - best = i; - } - } - - // Stall before mapping a new slab. - flush(); - - // Okay, we need to allocate. - if (!slabs_[best].Setup(slab_pos)) { - return 0; - } - return slabs_[best].Ptr(bufpos); -} - -u32 BufMapping::MapExtra(u32 bufpos, u32 sz, const std::function &flush) { - for (int i = 0; i < EXTRA_COUNT; ++i) { - // Might be likely to reuse larger buffers straddling slabs. - if (extra_[i].Matches(bufpos, sz)) { - return extra_[i].Ptr(); - } - } - - // Stall first, so we don't stomp existing RAM. - flush(); - - int i = extraOffset_; - extraOffset_ = (extraOffset_ + 1) % EXTRA_COUNT; - - if (!extra_[i].Alloc(bufpos, sz)) { - // Let's try to power on - hopefully none of these are still in use. - for (int i = 0; i < EXTRA_COUNT; ++i) { - extra_[i].Free(); - } - if (!extra_[i].Alloc(bufpos, sz)) { - return 0; - } - } - return extra_[i].Ptr(); -} - -bool BufMapping::SlabInfo::Alloc() { - u32 sz = SLAB_SIZE; - psp_pointer_ = userMemory.Alloc(sz, false, "Slab"); - if (psp_pointer_ == -1) { - psp_pointer_ = 0; - } - return psp_pointer_ != 0; -} - -void BufMapping::SlabInfo::Free() { - if (psp_pointer_) { - userMemory.Free(psp_pointer_); - psp_pointer_ = 0; - buf_pointer_ = 0; - last_used_ = 0; - } -} - -bool BufMapping::ExtraInfo::Alloc(u32 bufpos, u32 sz) { - // Make sure we've freed any previous allocation first. - Free(); - - u32 allocSize = sz; - psp_pointer_ = userMemory.Alloc(allocSize, false, "Straddle extra"); - if (psp_pointer_ == -1) { - psp_pointer_ = 0; - } - if (psp_pointer_ == 0) { - return false; - } - - buf_pointer_ = bufpos; - size_ = sz; - Memory::MemcpyUnchecked(psp_pointer_, pushbuf.data() + bufpos, sz); - return true; -} - -void BufMapping::ExtraInfo::Free() { - if (psp_pointer_) { - userMemory.Free(psp_pointer_); - psp_pointer_ = 0; - buf_pointer_ = 0; - } -} - -bool BufMapping::SlabInfo::Setup(u32 bufpos) { - // If it already has RAM, we're simply taking it over. Slabs come only in one size. - if (psp_pointer_ == 0) { - if (!Alloc()) { - return false; - } - } - - buf_pointer_ = bufpos; - u32 sz = std::min((u32)SLAB_SIZE, (u32)pushbuf.size() - bufpos); - Memory::MemcpyUnchecked(psp_pointer_, pushbuf.data() + bufpos, sz); - - slabGeneration_++; - last_used_ = slabGeneration_; - return true; -} - -int BufMapping::slabGeneration_ = 0; - static void FlushRegisters() { if (!lastRegisters.empty()) { Command last{CommandType::REGISTERS}; @@ -868,385 +560,4 @@ void NotifyFrame() { } } -void DumpExecute::SyncStall() { - gpu->UpdateStall(execListID, execListPos); - s64 listTicks = gpu->GetListTicks(execListID); - if (listTicks != -1) { - s64 nowTicks = CoreTiming::GetTicks(); - if (listTicks > nowTicks) { - currentMIPS->downcount -= listTicks - nowTicks; - } - } - - // Make sure downcount doesn't overflow. - CoreTiming::ForceCheck(); -} - -bool DumpExecute::SubmitCmds(void *p, u32 sz) { - if (execListBuf == 0) { - u32 allocSize = LIST_BUF_SIZE; - execListBuf = userMemory.Alloc(allocSize, "List buf"); - if (execListBuf == -1) { - execListBuf = 0; - } - if (execListBuf == 0) { - ERROR_LOG(SYSTEM, "Unable to allocate for display list"); - return false; - } - - execListPos = execListBuf; - Memory::Write_U32(GE_CMD_NOP << 24, execListPos); - execListPos += 4; - - gpu->EnableInterrupts(false); - auto optParam = PSPPointer::Create(0); - execListID = gpu->EnqueueList(execListBuf, execListPos, -1, optParam, false); - gpu->EnableInterrupts(true); - } - - u32 pendingSize = (int)execListQueue.size() * sizeof(u32); - // Validate space for jump. - u32 allocSize = pendingSize + sz + 8; - if (execListPos + allocSize >= execListBuf + LIST_BUF_SIZE) { - Memory::Write_U32((GE_CMD_BASE << 24) | ((execListBuf >> 8) & 0x00FF0000), execListPos); - Memory::Write_U32((GE_CMD_JUMP << 24) | (execListBuf & 0x00FFFFFF), execListPos + 4); - - execListPos = execListBuf; - - // Don't continue until we've stalled. - SyncStall(); - } - - Memory::MemcpyUnchecked(execListPos, execListQueue.data(), pendingSize); - execListPos += pendingSize; - u32 writePos = execListPos; - Memory::MemcpyUnchecked(execListPos, p, sz); - execListPos += sz; - - // TODO: Unfortunate. Maybe Texture commands should contain the bufw instead. - // The goal here is to realistically combine prims in dumps. Stalling for the bufw flushes. - u32_le *ops = (u32_le *)Memory::GetPointer(writePos); - for (u32 i = 0; i < sz / 4; ++i) { - u32 cmd = ops[i] >> 24; - if (cmd >= GE_CMD_TEXBUFWIDTH0 && cmd <= GE_CMD_TEXBUFWIDTH7) { - int level = cmd - GE_CMD_TEXBUFWIDTH0; - u16 bufw = ops[i] & 0xFFFF; - - // NOP the address part of the command to avoid a flush too. - if (bufw == lastBufw_[level]) - ops[i] = GE_CMD_NOP << 24; - else - ops[i] = (gstate.texbufwidth[level] & 0xFFFF0000) | bufw; - lastBufw_[level] = bufw; - } - - // Since we're here anyway, also NOP out texture addresses. - // This makes Step Tex not hit phantom textures. - if (cmd >= GE_CMD_TEXADDR0 && cmd <= GE_CMD_TEXADDR7) { - ops[i] = GE_CMD_NOP << 24; - } - } - - execListQueue.clear(); - - return true; -} - -void DumpExecute::SubmitListEnd() { - if (execListPos == 0) { - return; - } - - // There's always space for the end, same size as a jump. - Memory::Write_U32(GE_CMD_FINISH << 24, execListPos); - Memory::Write_U32(GE_CMD_END << 24, execListPos + 4); - execListPos += 8; - - SyncStall(); - gpu->ListSync(execListID, 0); -} - -void DumpExecute::Init(u32 ptr, u32 sz) { - gstate.Restore((u32_le *)(pushbuf.data() + ptr)); - gpu->ReapplyGfxState(); -} - -void DumpExecute::Registers(u32 ptr, u32 sz) { - SubmitCmds(pushbuf.data() + ptr, sz); -} - -void DumpExecute::Vertices(u32 ptr, u32 sz) { - u32 psp = execMapping.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this)); - if (psp == 0) { - ERROR_LOG(SYSTEM, "Unable to allocate for vertices"); - return; - } - - execListQueue.push_back((GE_CMD_BASE << 24) | ((psp >> 8) & 0x00FF0000)); - execListQueue.push_back((GE_CMD_VADDR << 24) | (psp & 0x00FFFFFF)); -} - -void DumpExecute::Indices(u32 ptr, u32 sz) { - u32 psp = execMapping.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this)); - if (psp == 0) { - ERROR_LOG(SYSTEM, "Unable to allocate for indices"); - return; - } - - execListQueue.push_back((GE_CMD_BASE << 24) | ((psp >> 8) & 0x00FF0000)); - execListQueue.push_back((GE_CMD_IADDR << 24) | (psp & 0x00FFFFFF)); -} - -void DumpExecute::Clut(u32 ptr, u32 sz) { - u32 psp = execMapping.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this)); - if (psp == 0) { - ERROR_LOG(SYSTEM, "Unable to allocate for clut"); - return; - } - - execListQueue.push_back((GE_CMD_CLUTADDRUPPER << 24) | ((psp >> 8) & 0x00FF0000)); - execListQueue.push_back((GE_CMD_CLUTADDR << 24) | (psp & 0x00FFFFFF)); -} - -void DumpExecute::TransferSrc(u32 ptr, u32 sz) { - u32 psp = execMapping.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this)); - if (psp == 0) { - ERROR_LOG(SYSTEM, "Unable to allocate for transfer"); - return; - } - - // Need to sync in order to access gstate.transfersrcw. - SyncStall(); - - execListQueue.push_back((gstate.transfersrcw & 0xFF00FFFF) | ((psp >> 8) & 0x00FF0000)); - execListQueue.push_back(((GE_CMD_TRANSFERSRC) << 24) | (psp & 0x00FFFFFF)); -} - -void DumpExecute::Memset(u32 ptr, u32 sz) { - struct MemsetCommand { - u32 dest; - int value; - u32 sz; - }; - - const MemsetCommand *data = (const MemsetCommand *)(pushbuf.data() + ptr); - - if (Memory::IsVRAMAddress(data->dest)) { - SyncStall(); - gpu->PerformMemorySet(data->dest, (u8)data->value, data->sz); - } -} - -void DumpExecute::MemcpyDest(u32 ptr, u32 sz) { - execMemcpyDest = *(const u32 *)(pushbuf.data() + ptr); -} - -void DumpExecute::Memcpy(u32 ptr, u32 sz) { - if (Memory::IsVRAMAddress(execMemcpyDest)) { - SyncStall(); - Memory::MemcpyUnchecked(execMemcpyDest, pushbuf.data() + ptr, sz); - gpu->PerformMemoryUpload(execMemcpyDest, sz); - } -} - -void DumpExecute::Texture(int level, u32 ptr, u32 sz) { - u32 psp = execMapping.Map(ptr, sz, std::bind(&DumpExecute::SyncStall, this)); - if (psp == 0) { - ERROR_LOG(SYSTEM, "Unable to allocate for texture"); - return; - } - - u32 bufwCmd = GE_CMD_TEXBUFWIDTH0 + level; - u32 addrCmd = GE_CMD_TEXADDR0 + level; - execListQueue.push_back((bufwCmd << 24) | ((psp >> 8) & 0x00FF0000) | lastBufw_[level]); - execListQueue.push_back((addrCmd << 24) | (psp & 0x00FFFFFF)); -} - -void DumpExecute::Framebuf(int level, u32 ptr, u32 sz) { - struct FramebufData { - u32 addr; - int bufw; - u32 flags; - u32 pad; - }; - - FramebufData *framebuf = (FramebufData *)(pushbuf.data() + ptr); - - u32 bufwCmd = GE_CMD_TEXBUFWIDTH0 + level; - u32 addrCmd = GE_CMD_TEXADDR0 + level; - execListQueue.push_back((bufwCmd << 24) | ((framebuf->addr >> 8) & 0x00FF0000) | framebuf->bufw); - execListQueue.push_back((addrCmd << 24) | (framebuf->addr & 0x00FFFFFF)); - lastBufw_[level] = framebuf->bufw; - - // And now also copy the data into VRAM (in case it wasn't actually rendered.) - u32 headerSize = (u32)sizeof(FramebufData); - u32 pspSize = sz - headerSize; - const bool isTarget = (framebuf->flags & 1) != 0; - // Could potentially always skip if !isTarget, but playing it safe for offset texture behavior. - if (Memory::IsValidRange(framebuf->addr, pspSize) && (!isTarget || !g_Config.bSoftwareRendering)) { - // Intentionally don't trigger an upload here. - Memory::MemcpyUnchecked(framebuf->addr, pushbuf.data() + ptr + headerSize, pspSize); - } -} - -void DumpExecute::Display(u32 ptr, u32 sz) { - struct DisplayBufData { - PSPPointer topaddr; - int linesize, pixelFormat; - }; - - DisplayBufData *disp = (DisplayBufData *)(pushbuf.data() + ptr); - - // Sync up drawing. - SyncStall(); - - __DisplaySetFramebuf(disp->topaddr.ptr, disp->linesize, disp->pixelFormat, 1); - __DisplaySetFramebuf(disp->topaddr.ptr, disp->linesize, disp->pixelFormat, 0); -} - -DumpExecute::~DumpExecute() { - execMemcpyDest = 0; - if (execListBuf) { - userMemory.Free(execListBuf); - execListBuf = 0; - } - execListPos = 0; - execMapping.Reset(); - - commands.clear(); - pushbuf.clear(); -} - -bool DumpExecute::Run() { - for (const Command &cmd : commands) { - switch (cmd.type) { - case CommandType::INIT: - Init(cmd.ptr, cmd.sz); - break; - - case CommandType::REGISTERS: - Registers(cmd.ptr, cmd.sz); - break; - - case CommandType::VERTICES: - Vertices(cmd.ptr, cmd.sz); - break; - - case CommandType::INDICES: - Indices(cmd.ptr, cmd.sz); - break; - - case CommandType::CLUT: - Clut(cmd.ptr, cmd.sz); - break; - - case CommandType::TRANSFERSRC: - TransferSrc(cmd.ptr, cmd.sz); - break; - - case CommandType::MEMSET: - Memset(cmd.ptr, cmd.sz); - break; - - case CommandType::MEMCPYDEST: - MemcpyDest(cmd.ptr, cmd.sz); - break; - - case CommandType::MEMCPYDATA: - Memcpy(cmd.ptr, cmd.sz); - break; - - case CommandType::TEXTURE0: - case CommandType::TEXTURE1: - case CommandType::TEXTURE2: - case CommandType::TEXTURE3: - case CommandType::TEXTURE4: - case CommandType::TEXTURE5: - case CommandType::TEXTURE6: - case CommandType::TEXTURE7: - Texture((int)cmd.type - (int)CommandType::TEXTURE0, cmd.ptr, cmd.sz); - break; - - case CommandType::FRAMEBUF0: - case CommandType::FRAMEBUF1: - case CommandType::FRAMEBUF2: - case CommandType::FRAMEBUF3: - case CommandType::FRAMEBUF4: - case CommandType::FRAMEBUF5: - case CommandType::FRAMEBUF6: - case CommandType::FRAMEBUF7: - Framebuf((int)cmd.type - (int)CommandType::FRAMEBUF0, cmd.ptr, cmd.sz); - break; - - case CommandType::DISPLAY: - Display(cmd.ptr, cmd.sz); - break; - - default: - ERROR_LOG(SYSTEM, "Unsupported GE dump command: %d", (int)cmd.type); - return false; - } - } - - SubmitListEnd(); - return true; -} - -static bool ReadCompressed(u32 fp, void *dest, size_t sz) { - u32 compressed_size = 0; - if (pspFileSystem.ReadFile(fp, (u8 *)&compressed_size, sizeof(compressed_size)) != sizeof(compressed_size)) { - return false; - } - - u8 *compressed = new u8[compressed_size]; - if (pspFileSystem.ReadFile(fp, compressed, compressed_size) != compressed_size) { - delete [] compressed; - return false; - } - - size_t real_size = sz; - snappy_uncompress((const char *)compressed, compressed_size, (char *)dest, &real_size); - delete [] compressed; - - return real_size == sz; -} - -bool RunMountedReplay(const std::string &filename) { - _assert_msg_(SYSTEM, !active && !nextFrame, "Cannot run replay while recording."); - - u32 fp = pspFileSystem.OpenFile(filename, FILEACCESS_READ); - u8 header[8]{}; - int version = 0; - pspFileSystem.ReadFile(fp, header, sizeof(header)); - pspFileSystem.ReadFile(fp, (u8 *)&version, sizeof(version)); - - if (memcmp(header, HEADER, sizeof(header)) != 0 || version > VERSION || version < MIN_VERSION) { - ERROR_LOG(SYSTEM, "Invalid GE dump or unsupported version"); - pspFileSystem.CloseFile(fp); - return false; - } - - u32 sz = 0; - pspFileSystem.ReadFile(fp, (u8 *)&sz, sizeof(sz)); - u32 bufsz = 0; - pspFileSystem.ReadFile(fp, (u8 *)&bufsz, sizeof(bufsz)); - - commands.resize(sz); - pushbuf.resize(bufsz); - - bool truncated = false; - truncated = truncated || !ReadCompressed(fp, commands.data(), sizeof(Command) * sz); - truncated = truncated || !ReadCompressed(fp, pushbuf.data(), bufsz); - - pspFileSystem.CloseFile(fp); - - if (truncated) { - ERROR_LOG(SYSTEM, "Truncated GE dump"); - return false; - } - - DumpExecute executor; - return executor.Run(); -} - }; diff --git a/GPU/Debugger/Record.h b/GPU/Debugger/Record.h index 020ce2ca8c..ce15aa68d0 100644 --- a/GPU/Debugger/Record.h +++ b/GPU/Debugger/Record.h @@ -36,6 +36,4 @@ void NotifyUpload(u32 dest, u32 sz); void NotifyDisplay(u32 addr, int stride, int fmt); void NotifyFrame(); -bool RunMountedReplay(const std::string &filename); - }; diff --git a/GPU/Debugger/RecordFormat.h b/GPU/Debugger/RecordFormat.h new file mode 100644 index 0000000000..aeb7791fe2 --- /dev/null +++ b/GPU/Debugger/RecordFormat.h @@ -0,0 +1,72 @@ +// Copyright (c) 2017- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#pragma once + +#include "Common/CommonTypes.h" + +namespace GPURecord { + +static const char *HEADER = "PPSSPPGE"; +// Version 1: Uncompressed +// Version 2: Uses snappy +// Version 3: Adds FRAMEBUF0-FRAMEBUF9 +static const int VERSION = 3; +static const int MIN_VERSION = 2; + +enum class CommandType : u8 { + INIT = 0, + REGISTERS = 1, + VERTICES = 2, + INDICES = 3, + CLUT = 4, + TRANSFERSRC = 5, + MEMSET = 6, + MEMCPYDEST = 7, + MEMCPYDATA = 8, + DISPLAY = 9, + + TEXTURE0 = 0x10, + TEXTURE1 = 0x11, + TEXTURE2 = 0x12, + TEXTURE3 = 0x13, + TEXTURE4 = 0x14, + TEXTURE5 = 0x15, + TEXTURE6 = 0x16, + TEXTURE7 = 0x17, + + FRAMEBUF0 = 0x18, + FRAMEBUF1 = 0x19, + FRAMEBUF2 = 0x1A, + FRAMEBUF3 = 0x1B, + FRAMEBUF4 = 0x1C, + FRAMEBUF5 = 0x1D, + FRAMEBUF6 = 0x1E, + FRAMEBUF7 = 0x1F, +}; + +#pragma pack(push, 1) + +struct Command { + CommandType type; + u32 sz; + u32 ptr; +}; + +#pragma pack(pop) + +}; diff --git a/GPU/GPU.vcxproj b/GPU/GPU.vcxproj index eabc46f4ba..af8feef17b 100644 --- a/GPU/GPU.vcxproj +++ b/GPU/GPU.vcxproj @@ -406,7 +406,9 @@ + + @@ -582,6 +584,7 @@ + diff --git a/GPU/GPU.vcxproj.filters b/GPU/GPU.vcxproj.filters index 76b0113d10..b6f35b5a2b 100644 --- a/GPU/GPU.vcxproj.filters +++ b/GPU/GPU.vcxproj.filters @@ -276,6 +276,12 @@ Debugger + + Debugger + + + Debugger + @@ -551,5 +557,8 @@ GLES + + Debugger + \ No newline at end of file diff --git a/GPU/GPUCommon.cpp b/GPU/GPUCommon.cpp index d9c6b80e76..8b5c16af1d 100644 --- a/GPU/GPUCommon.cpp +++ b/GPU/GPUCommon.cpp @@ -188,10 +188,10 @@ const CommonCommandTableEntry commonCommandTable[] = { // Viewport. { GE_CMD_OFFSETX, FLAG_FLUSHBEFOREONCHANGE, DIRTY_VIEWPORTSCISSOR_STATE | DIRTY_CULLRANGE }, { GE_CMD_OFFSETY, FLAG_FLUSHBEFOREONCHANGE, DIRTY_VIEWPORTSCISSOR_STATE | DIRTY_CULLRANGE }, - { GE_CMD_VIEWPORTXSCALE, FLAG_FLUSHBEFOREONCHANGE, DIRTY_FRAMEBUF | DIRTY_TEXTURE_PARAMS | DIRTY_CULLRANGE | DIRTY_VIEWPORTSCISSOR_STATE }, - { GE_CMD_VIEWPORTYSCALE, FLAG_FLUSHBEFOREONCHANGE, DIRTY_FRAMEBUF | DIRTY_TEXTURE_PARAMS | DIRTY_CULLRANGE | DIRTY_VIEWPORTSCISSOR_STATE }, - { GE_CMD_VIEWPORTXCENTER, FLAG_FLUSHBEFOREONCHANGE, DIRTY_FRAMEBUF | DIRTY_TEXTURE_PARAMS | DIRTY_CULLRANGE | DIRTY_VIEWPORTSCISSOR_STATE }, - { GE_CMD_VIEWPORTYCENTER, FLAG_FLUSHBEFOREONCHANGE, DIRTY_FRAMEBUF | DIRTY_TEXTURE_PARAMS | DIRTY_CULLRANGE | DIRTY_VIEWPORTSCISSOR_STATE }, + { GE_CMD_VIEWPORTXSCALE, FLAG_FLUSHBEFOREONCHANGE, DIRTY_FRAMEBUF | DIRTY_TEXTURE_PARAMS | DIRTY_CULLRANGE | DIRTY_PROJMATRIX | DIRTY_VIEWPORTSCISSOR_STATE }, + { GE_CMD_VIEWPORTYSCALE, FLAG_FLUSHBEFOREONCHANGE, DIRTY_FRAMEBUF | DIRTY_TEXTURE_PARAMS | DIRTY_CULLRANGE | DIRTY_PROJMATRIX | DIRTY_VIEWPORTSCISSOR_STATE }, + { GE_CMD_VIEWPORTXCENTER, FLAG_FLUSHBEFOREONCHANGE, DIRTY_FRAMEBUF | DIRTY_TEXTURE_PARAMS | DIRTY_CULLRANGE | DIRTY_PROJMATRIX | DIRTY_VIEWPORTSCISSOR_STATE }, + { GE_CMD_VIEWPORTYCENTER, FLAG_FLUSHBEFOREONCHANGE, DIRTY_FRAMEBUF | DIRTY_TEXTURE_PARAMS | DIRTY_CULLRANGE | DIRTY_PROJMATRIX | DIRTY_VIEWPORTSCISSOR_STATE }, { GE_CMD_VIEWPORTZSCALE, FLAG_FLUSHBEFOREONCHANGE, DIRTY_FRAMEBUF | DIRTY_TEXTURE_PARAMS | DIRTY_CULLRANGE | DIRTY_DEPTHRANGE | DIRTY_PROJMATRIX | DIRTY_VIEWPORTSCISSOR_STATE }, { GE_CMD_VIEWPORTZCENTER, FLAG_FLUSHBEFOREONCHANGE, DIRTY_FRAMEBUF | DIRTY_TEXTURE_PARAMS | DIRTY_CULLRANGE | DIRTY_DEPTHRANGE | DIRTY_PROJMATRIX | DIRTY_VIEWPORTSCISSOR_STATE }, { GE_CMD_DEPTHCLAMPENABLE, FLAG_FLUSHBEFOREONCHANGE, DIRTY_VIEWPORTSCISSOR_STATE | DIRTY_CULLRANGE | DIRTY_RASTER_STATE }, diff --git a/UWP/GPU_UWP/GPU_UWP.vcxproj b/UWP/GPU_UWP/GPU_UWP.vcxproj index 9dd5a945de..73a9f1ae43 100644 --- a/UWP/GPU_UWP/GPU_UWP.vcxproj +++ b/UWP/GPU_UWP/GPU_UWP.vcxproj @@ -413,7 +413,9 @@ + + @@ -472,6 +474,7 @@ + diff --git a/UWP/GPU_UWP/GPU_UWP.vcxproj.filters b/UWP/GPU_UWP/GPU_UWP.vcxproj.filters index 5f71cb0d67..b519821e34 100644 --- a/UWP/GPU_UWP/GPU_UWP.vcxproj.filters +++ b/UWP/GPU_UWP/GPU_UWP.vcxproj.filters @@ -39,6 +39,7 @@ + @@ -91,7 +92,9 @@ + + diff --git a/Windows/GPU/D3D11Context.cpp b/Windows/GPU/D3D11Context.cpp index 49cd1bbdf8..fef1438fb6 100644 --- a/Windows/GPU/D3D11Context.cpp +++ b/Windows/GPU/D3D11Context.cpp @@ -6,6 +6,7 @@ #include #include "base/logging.h" +#include "Base/display.h" #include "util/text/utf8.h" #include "i18n/i18n.h" @@ -51,6 +52,9 @@ void D3D11Context::SwapInterval(int interval) { HRESULT D3D11Context::CreateTheDevice(IDXGIAdapter *adapter) { bool windowed = true; + // D3D11 has no need for display rotation. + g_display_rotation = DisplayRotation::ROTATE_0; + g_display_rot_matrix.setIdentity(); #if defined(_DEBUG) && !defined(_M_ARM) && !defined(_M_ARM64) UINT createDeviceFlags = D3D11_CREATE_DEVICE_DEBUG; #else diff --git a/Windows/GPU/D3D9Context.cpp b/Windows/GPU/D3D9Context.cpp index c21a6d7c5d..4317fd1328 100644 --- a/Windows/GPU/D3D9Context.cpp +++ b/Windows/GPU/D3D9Context.cpp @@ -6,6 +6,7 @@ #include "gfx/d3d9_state.h" #include "base/logging.h" +#include "base/display.h" #include "util/text/utf8.h" #include "i18n/i18n.h" @@ -62,6 +63,10 @@ bool D3D9Context::Init(HINSTANCE hInst, HWND wnd, std::string *error_message) { bool windowed = true; hWnd_ = wnd; + // D3D9 has no need for display rotation. + g_display_rotation = DisplayRotation::ROTATE_0; + g_display_rot_matrix.setIdentity(); + DIRECT3DCREATE9EX g_pfnCreate9ex; hD3D9_ = LoadLibrary(TEXT("d3d9.dll")); diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 47174b4a10..985b8efa8e 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -245,6 +245,7 @@ EXEC_AND_LIB_FILES := \ $(SRC)/GPU/Common/ShaderUniforms.cpp \ $(SRC)/GPU/Debugger/Breakpoints.cpp \ $(SRC)/GPU/Debugger/Debugger.cpp \ + $(SRC)/GPU/Debugger/Playback.cpp \ $(SRC)/GPU/Debugger/Record.cpp \ $(SRC)/GPU/Debugger/Stepping.cpp \ $(SRC)/GPU/GLES/FramebufferManagerGLES.cpp \ diff --git a/android/jni/AndroidJavaGLContext.cpp b/android/jni/AndroidJavaGLContext.cpp index 2d463e76dc..f03c5b864d 100644 --- a/android/jni/AndroidJavaGLContext.cpp +++ b/android/jni/AndroidJavaGLContext.cpp @@ -1,6 +1,7 @@ #include #include "AndroidJavaGLContext.h" +#include "base/display.h" #include "base/NativeApp.h" #include "gfx_es2/gpu_features.h" #include "Core/ConfigValues.h" @@ -13,6 +14,9 @@ AndroidJavaEGLGraphicsContext::AndroidJavaEGLGraphicsContext() { bool AndroidJavaEGLGraphicsContext::InitFromRenderThread(ANativeWindow *wnd, int desiredBackbufferSizeX, int desiredBackbufferSizeY, int backbufferFormat, int androidVersion) { ILOG("AndroidJavaEGLGraphicsContext::InitFromRenderThread"); CheckGLExtensions(); + // OpenGL handles rotated rendering in the driver. + g_display_rotation = DisplayRotation::ROTATE_0; + g_display_rot_matrix.setIdentity(); draw_ = Draw::T3DCreateGLContext(); renderManager_ = (GLRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER); bool success = draw_->CreatePresets(); diff --git a/ext/native/thin3d/GLQueueRunner.cpp b/ext/native/thin3d/GLQueueRunner.cpp index cf4dd780dd..5fd98a49f6 100644 --- a/ext/native/thin3d/GLQueueRunner.cpp +++ b/ext/native/thin3d/GLQueueRunner.cpp @@ -1,3 +1,4 @@ +#include #include "Common/MemoryUtil.h" #include "Core/Reporting.h" #include "GLQueueRunner.h" @@ -1318,25 +1319,51 @@ void GLQueueRunner::PerformReadback(const GLRStep &pass) { void GLQueueRunner::PerformReadbackImage(const GLRStep &pass) { GLRTexture *tex = pass.readback_image.texture; - glBindTexture(GL_TEXTURE_2D, tex->texture); - - CHECK_GL_ERROR_IF_DEBUG(); - #ifndef USING_GLES2 - int pixelStride = pass.readback_image.srcRect.w; - glPixelStorei(GL_PACK_ALIGNMENT, 4); - GLRect2D rect = pass.readback_image.srcRect; - int size = 4 * rect.w * rect.h; - if (size > readbackBufferSize_) { - delete[] readbackBuffer_; - readbackBuffer_ = new uint8_t[size]; - readbackBufferSize_ = size; - } + if (gl_extensions.VersionGEThan(4, 5)) { + int size = 4 * rect.w * rect.h; + if (size > readbackBufferSize_) { + delete[] readbackBuffer_; + readbackBuffer_ = new uint8_t[size]; + readbackBufferSize_ = size; + } - glPixelStorei(GL_PACK_ALIGNMENT, 4); - glGetTexImage(GL_TEXTURE_2D, pass.readback_image.mipLevel, GL_RGBA, GL_UNSIGNED_BYTE, readbackBuffer_); + glPixelStorei(GL_PACK_ALIGNMENT, 4); + glGetTextureSubImage(tex->texture, pass.readback_image.mipLevel, rect.x, rect.y, 0, rect.w, rect.h, 1, GL_RGBA, GL_UNSIGNED_BYTE, readbackBufferSize_, readbackBuffer_); + } else { + glBindTexture(GL_TEXTURE_2D, tex->texture); + + CHECK_GL_ERROR_IF_DEBUG(); + + GLint w, h; + // This is only used for debugging (currently), and GL doesn't support a subrectangle. + glGetTexLevelParameteriv(GL_TEXTURE_2D, pass.readback_image.mipLevel, GL_TEXTURE_WIDTH, &w); + glGetTexLevelParameteriv(GL_TEXTURE_2D, pass.readback_image.mipLevel, GL_TEXTURE_HEIGHT, &h); + + int size = 4 * std::max((int)w, rect.x + rect.w) * std::max((int)h, rect.h); + if (size > readbackBufferSize_) { + delete[] readbackBuffer_; + readbackBuffer_ = new uint8_t[size]; + readbackBufferSize_ = size; + } + + glPixelStorei(GL_PACK_ALIGNMENT, 4); + glPixelStorei(GL_PACK_ROW_LENGTH, rect.x + rect.w); + glGetTexImage(GL_TEXTURE_2D, pass.readback_image.mipLevel, GL_RGBA, GL_UNSIGNED_BYTE, readbackBuffer_); + glPixelStorei(GL_PACK_ROW_LENGTH, 0); + + if (rect.x != 0 || rect.y != 0) { + int dstStride = 4 * rect.w; + int srcStride = 4 * (rect.x + rect.w); + int xoff = 4 * rect.x; + int yoff = rect.y * srcStride; + for (int y = 0; y < rect.h; ++y) { + memmove(readbackBuffer_ + h * dstStride, readbackBuffer_ + yoff + h * srcStride + xoff, dstStride); + } + } + } #endif CHECK_GL_ERROR_IF_DEBUG(); diff --git a/ext/native/ui/view.cpp b/ext/native/ui/view.cpp index 2a0fc2cbb6..378290f951 100644 --- a/ext/native/ui/view.cpp +++ b/ext/native/ui/view.cpp @@ -60,9 +60,22 @@ void DispatchEvents() { } void RemoveQueuedEvents(View *v) { - for (size_t i = 0; i < g_dispatchQueue.size(); i++) { - if (g_dispatchQueue[i].params.v == v) - g_dispatchQueue.erase(g_dispatchQueue.begin() + i); + for (auto it = g_dispatchQueue.begin(); it != g_dispatchQueue.end(); ) { + if (it->params.v == v) { + it = g_dispatchQueue.erase(it); + } else { + ++it; + } + } +} + +void RemoveQueuedEvents(Event *e) { + for (auto it = g_dispatchQueue.begin(); it != g_dispatchQueue.end(); ) { + if (it->e == e) { + it = g_dispatchQueue.erase(it); + } else { + ++it; + } } } @@ -157,6 +170,11 @@ EventReturn Event::Dispatch(EventParams &e) { return UI::EVENT_SKIPPED; } +Event::~Event() { + handlers_.clear(); + RemoveQueuedEvents(this); +} + View::~View() { if (HasFocus()) SetFocusedView(0); diff --git a/ext/native/ui/view.h b/ext/native/ui/view.h index 45d0538d44..c59110c0d2 100644 --- a/ext/native/ui/view.h +++ b/ext/native/ui/view.h @@ -240,9 +240,7 @@ struct HandlerRegistration { class Event { public: Event() {} - ~Event() { - handlers_.clear(); - } + ~Event(); // Call this from input thread or whatever, it doesn't matter void Trigger(EventParams &e); // Call this from UI thread diff --git a/ios/AppDelegate.h b/ios/AppDelegate.h index 263e85d369..feb65efbf4 100644 --- a/ios/AppDelegate.h +++ b/ios/AppDelegate.h @@ -7,6 +7,7 @@ @interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; +@property (strong, nonatomic) UIScreen *screen; @property (strong, nonatomic) ViewController *viewController; diff --git a/ios/DisplayManager.h b/ios/DisplayManager.h new file mode 100644 index 0000000000..fb46707a19 --- /dev/null +++ b/ios/DisplayManager.h @@ -0,0 +1,24 @@ +// +// DisplayManager.h +// native +// +// Created by xieyi on 2019/6/9. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface DisplayManager : NSObject + +- (void)setupDisplayListener; +- (void)updateResolution:(UIScreen *)screen; + +@property (nonatomic, strong) UIScreen *mainScreen; + +@property (class, readonly, strong) DisplayManager *shared; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/DisplayManager.mm b/ios/DisplayManager.mm new file mode 100644 index 0000000000..495353a6df --- /dev/null +++ b/ios/DisplayManager.mm @@ -0,0 +1,179 @@ +// +// DisplayManager.m +// native +// +// Created by xieyi on 2019/6/9. +// + +#import "DisplayManager.h" +#import "ViewController.h" +#import "AppDelegate.h" +#include "base/display.h" +#include "base/NativeApp.h" +#include "Core/System.h" +#import + +#define IS_IPAD() ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) + +@interface DisplayManager () + +@property BOOL listenerActive; +@property (atomic, retain) NSMutableArray *extDisplays; +@property CGRect originalFrame; +@property CGRect originalBounds; +@property CGAffineTransform originalTransform; + +- (void)updateScreen:(UIScreen *)screen; + +@end + +@implementation DisplayManager + +- (instancetype)init +{ + self = [super init]; + if (self) { + [self setListenerActive:NO]; + [self setExtDisplays:[[NSMutableArray alloc] init]]; + } + return self; +} + ++ (DisplayManager *)shared { + static DisplayManager *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[DisplayManager alloc] init]; + }); + return sharedInstance; +} + +- (void)setupDisplayListener { + // Disable external display by default + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"enable_external_display"] == NO) { + return; + } + if ([self listenerActive]) { + NSLog(@"setupDisplayListener already called"); + return; + } + NSLog(@"Setting up display manager"); + [self setMainScreen:[UIScreen mainScreen]]; + UIWindow *gameWindow = [(AppDelegate *)[[UIApplication sharedApplication] delegate] window]; + [self setOriginalFrame: [gameWindow frame]]; + [self setOriginalBounds:[gameWindow bounds]]; + [self setOriginalTransform:[gameWindow transform]]; + // Display connected + [[NSNotificationCenter defaultCenter] addObserverForName:UIScreenDidConnectNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull notification) { + UIScreen *screen = (UIScreen *) notification.object; + NSLog(@"New display connected: %@", [screen debugDescription]); + [[self extDisplays] addObject:screen]; + // Do not switch to second connected display + if ([self mainScreen] != [UIScreen mainScreen]) { + return; + } + // Ignore mute switch when connected to external display + NSError *error = nil; + [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&error]; + [self updateScreen:screen]; + }]; + // Display disconnected + [[NSNotificationCenter defaultCenter] addObserverForName:UIScreenDidDisconnectNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull notification) { + UIScreen *screen = (UIScreen *) notification.object; + NSLog(@"Display disconnected: %@", [screen debugDescription]); + if ([[self extDisplays] containsObject:screen]) { + [[self extDisplays] removeObject:screen]; + } + if ([[self extDisplays] count] > 0) { + UIScreen *newScreen = [[self extDisplays] lastObject]; + [self updateScreen:newScreen]; + } else { + NSError *error = nil; + [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryAmbient error:&error]; + [self updateScreen:[UIScreen mainScreen]]; + } + }]; + [self setListenerActive:YES]; +} + +- (void)updateScreen:(UIScreen *)screen { + [self setMainScreen:screen]; + UIWindow *gameWindow = [(AppDelegate *)[[UIApplication sharedApplication] delegate] window]; + // Hide before moving window to external display, otherwise iPhone won't switch to it + [gameWindow setHidden:YES]; + [gameWindow setScreen:screen]; + // Set optimal resolution + // Dispatch later to prevent "no window is preset" error + dispatch_async(dispatch_get_main_queue(), ^{ + if (screen != [UIScreen mainScreen]) { + NSUInteger count = [[screen availableModes] count]; + UIScreenMode* mode = [screen availableModes][count - 1]; + [screen setCurrentMode:mode]; + mode = [screen currentMode]; + // Fix overscan + // TODO: Hacky solution. Screen is still scaled even if UIScreenOverscanCompensationNone is set. + [screen setOverscanCompensation:UIScreenOverscanCompensationNone]; + CGSize fullSize = mode.size; + UIEdgeInsets insets = [screen overscanCompensationInsets]; + fullSize.width -= insets.left + insets.right; + fullSize.height -= insets.top + insets.bottom; + [gameWindow setFrame:CGRectMake(insets.left, insets.top, fullSize.width, fullSize.height)]; + [gameWindow setBounds:CGRectMake(0, 0, fullSize.width, fullSize.height)]; + [self updateResolution:screen]; + [gameWindow setTransform:CGAffineTransformMakeScale(mode.size.width / fullSize.width, mode.size.height / fullSize.height)]; + } else { + [gameWindow setTransform:[self originalTransform]]; + [gameWindow setFrame:[self originalFrame]]; + [gameWindow setBounds:[self originalBounds]]; + [self updateResolution:screen]; + } + [gameWindow setHidden:NO]; + }); +} + +- (void)updateResolution:(UIScreen *)screen { + float scale = screen.scale; + + if ([screen respondsToSelector:@selector(nativeScale)]) { + scale = screen.nativeScale; + } + + CGSize size = screen.applicationFrame.size; + + if (size.height > size.width) { + float h = size.height; + size.height = size.width; + size.width = h; + } + + if (screen == [UIScreen mainScreen]) { + g_dpi = (IS_IPAD() ? 200.0f : 150.0f) * scale; + } else { + float diagonal = sqrt(size.height * size.height + size.width * size.width); + g_dpi = diagonal * scale * 0.1f; + } + g_dpi_scale_x = 240.0f / g_dpi; + g_dpi_scale_y = 240.0f / g_dpi; + g_dpi_scale_real_x = g_dpi_scale_x; + g_dpi_scale_real_y = g_dpi_scale_y; + pixel_xres = size.width * scale; + pixel_yres = size.height * scale; + + dp_xres = pixel_xres * g_dpi_scale_x; + dp_yres = pixel_yres * g_dpi_scale_y; + + pixel_in_dps_x = (float)pixel_xres / (float)dp_xres; + pixel_in_dps_y = (float)pixel_yres / (float)dp_yres; + + [[sharedViewController view] setContentScaleFactor:scale]; + + // PSP native resize + PSP_CoreParameter().pixelWidth = pixel_xres; + PSP_CoreParameter().pixelHeight = pixel_yres; + + NativeResized(); + + NSLog(@"Updated display resolution: (%d, %d) @%.1fx", pixel_xres, pixel_yres, scale); +} + +@end diff --git a/ios/Settings.bundle/Root.plist b/ios/Settings.bundle/Root.plist new file mode 100644 index 0000000000..627d9c50de --- /dev/null +++ b/ios/Settings.bundle/Root.plist @@ -0,0 +1,21 @@ + + + + + StringsTable + Root + PreferenceSpecifiers + + + Type + PSToggleSwitchSpecifier + Title + enable_external_display + Key + enable_external_display + DefaultValue + + + + + diff --git a/ios/Settings.bundle/en.lproj/Root.strings b/ios/Settings.bundle/en.lproj/Root.strings new file mode 100644 index 0000000000..cc9f6b61f9 Binary files /dev/null and b/ios/Settings.bundle/en.lproj/Root.strings differ diff --git a/ios/ViewController.mm b/ios/ViewController.mm index 5eba171e2b..ad334f0d8d 100644 --- a/ios/ViewController.mm +++ b/ios/ViewController.mm @@ -5,7 +5,9 @@ // Modified by xSacha // +#import "AppDelegate.h" #import "ViewController.h" +#import "DisplayManager.h" #import "SubtleVolume.h" #import #include @@ -143,8 +145,10 @@ static GraphicsContext *graphicsContext; - (void)viewDidLoad { [super viewDidLoad]; + [[DisplayManager shared] setupDisplayListener]; - self.view.frame = [[UIScreen mainScreen] bounds]; + UIScreen* screen = [(AppDelegate*)[UIApplication sharedApplication].delegate screen]; + self.view.frame = [screen bounds]; self.view.multipleTouchEnabled = YES; self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3]; @@ -158,36 +162,7 @@ static GraphicsContext *graphicsContext; [EAGLContext setCurrentContext:self.context]; self.preferredFramesPerSecond = 60; - // Might be useful for a speed boot, sacrificing resolution: - // view.contentScaleFactor = 1.0; - - float scale = [UIScreen mainScreen].scale; - - if ([[UIScreen mainScreen] respondsToSelector:@selector(nativeScale)]) { - scale = [UIScreen mainScreen].nativeScale; - } - - CGSize size = [[UIApplication sharedApplication].delegate window].frame.size; - - if (size.height > size.width) { - float h = size.height; - size.height = size.width; - size.width = h; - } - - g_dpi = (IS_IPAD() ? 200.0f : 150.0f) * scale; - g_dpi_scale_x = 240.0f / g_dpi; - g_dpi_scale_y = 240.0f / g_dpi; - g_dpi_scale_real_x = g_dpi_scale_x; - g_dpi_scale_real_y = g_dpi_scale_y; - pixel_xres = size.width * scale; - pixel_yres = size.height * scale; - - dp_xres = pixel_xres * g_dpi_scale_x; - dp_yres = pixel_yres * g_dpi_scale_y; - - pixel_in_dps_x = (float)pixel_xres / (float)dp_xres; - pixel_in_dps_y = (float)pixel_yres / (float)dp_yres; + [[DisplayManager shared] updateResolution:[UIScreen mainScreen]]; graphicsContext = new IOSGraphicsContext(); @@ -238,7 +213,6 @@ static GraphicsContext *graphicsContext; time_update(); } - threadStopped = true; ILOG("Emulation thread shutting down\n"); NativeShutdownGraphics(); @@ -246,6 +220,8 @@ static GraphicsContext *graphicsContext; // Also ask the main thread to stop, so it doesn't hang waiting for a new frame. ILOG("Emulation thread stopping\n"); graphicsContext->StopThread(); + + threadStopped = true; }); } diff --git a/ios/main.mm b/ios/main.mm index ce1803e023..e52f3f8bf9 100644 --- a/ios/main.mm +++ b/ios/main.mm @@ -61,7 +61,10 @@ bool System_GetPropertyBool(SystemProperty prop) { void System_SendMessage(const char *command, const char *parameter) { if (!strcmp(command, "finish")) { - exit(0); + dispatch_async(dispatch_get_main_queue(), ^{ + [sharedViewController shutdown]; + exit(0); + }); } } diff --git a/libretro/Makefile.common b/libretro/Makefile.common index e48ddbf990..cf2d07681c 100644 --- a/libretro/Makefile.common +++ b/libretro/Makefile.common @@ -169,6 +169,7 @@ SOURCES_CXX += \ $(COMMONDIR)/ColorConv.cpp \ $(GPUDIR)/Debugger/Breakpoints.cpp \ $(GPUDIR)/Debugger/Debugger.cpp \ + $(GPUDIR)/Debugger/Playback.cpp \ $(GPUDIR)/Debugger/Record.cpp \ $(GPUDIR)/Debugger/Stepping.cpp \ $(GPUDIR)/Common/TextureCacheCommon.cpp \