mirror of
https://github.com/hrydgard/ppsspp.git
synced 2024-11-28 16:00:58 +00:00
Merge branch 'master' into compat_openxr_fixes
This commit is contained in:
commit
2be8f46a0c
1
.github/workflows/build.yml
vendored
1
.github/workflows/build.yml
vendored
@ -69,6 +69,7 @@ jobs:
|
||||
path: ppsspp/
|
||||
|
||||
build-uwp:
|
||||
if: ${{false}} # Temporarily disable
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
@ -1136,11 +1136,21 @@ bool VulkanContext::InitSwapchain() {
|
||||
INFO_LOG(G3D, "Transform supported: %s current: %s chosen: %s", supportedTransforms.c_str(), currentTransform.c_str(), preTransformStr.c_str());
|
||||
|
||||
if (physicalDeviceProperties_[physical_device_].properties.vendorID == VULKAN_VENDOR_IMGTEC) {
|
||||
INFO_LOG(G3D, "Applying PowerVR hack (rounding off the width!)");
|
||||
// Swap chain width hack to avoid issue #11743 (PowerVR driver bug).
|
||||
// To keep the size consistent even with pretransform, do this after the swap. Should be fine.
|
||||
// This is fixed in newer PowerVR drivers but I don't know the cutoff.
|
||||
swapChainExtent_.width &= ~31;
|
||||
u32 driverVersion = physicalDeviceProperties_[physical_device_].properties.driverVersion;
|
||||
// Cutoff the hack at driver version 1.386.1368 (0x00582558, see issue #15773).
|
||||
if (driverVersion < 0x00582558) {
|
||||
INFO_LOG(G3D, "Applying PowerVR hack (rounding off the width!) driverVersion=%08x", driverVersion);
|
||||
// Swap chain width hack to avoid issue #11743 (PowerVR driver bug).
|
||||
// To keep the size consistent even with pretransform, do this after the swap. Should be fine.
|
||||
// This is fixed in newer PowerVR drivers but I don't know the cutoff.
|
||||
swapChainExtent_.width &= ~31;
|
||||
|
||||
// TODO: Also modify display_xres/display_yres appropriately for scissors to match.
|
||||
// This will get a bit messy. Ideally we should remove that logic from app-android.cpp
|
||||
// and move it here, but the OpenGL code still needs it.
|
||||
} else {
|
||||
INFO_LOG(G3D, "PowerVR driver version new enough (%08x), not applying swapchain width hack", driverVersion);
|
||||
}
|
||||
}
|
||||
|
||||
VkSwapchainCreateInfoKHR swap_chain_info{ VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR };
|
||||
|
@ -172,7 +172,7 @@ bool GenerateFragmentShader(const FShaderID &id, char *buffer, const ShaderLangu
|
||||
bool fetchFramebuffer = needFramebufferRead && id.Bit(FS_BIT_USE_FRAMEBUFFER_FETCH);
|
||||
bool readFramebufferTex = needFramebufferRead && !id.Bit(FS_BIT_USE_FRAMEBUFFER_FETCH);
|
||||
|
||||
if (fetchFramebuffer && (compat.shaderLanguage != GLSL_VULKAN || compat.shaderLanguage != GLSL_3xx)) {
|
||||
if (fetchFramebuffer && compat.shaderLanguage != GLSL_VULKAN && (compat.shaderLanguage != GLSL_3xx || !compat.lastFragData)) {
|
||||
*errorString = "framebuffer fetch requires GLSL: vulkan or 3xx";
|
||||
return false;
|
||||
}
|
||||
@ -568,7 +568,7 @@ bool GenerateFragmentShader(const FShaderID &id, char *buffer, const ShaderLangu
|
||||
if (compat.shaderLanguage == GLSL_3xx) {
|
||||
WRITE(p, " lowp vec4 destColor = %s;\n", compat.lastFragData);
|
||||
} else if (compat.shaderLanguage == GLSL_VULKAN) {
|
||||
WRITE(p, " lowp vec4 destColor = subpassLoad(inputColor);\n", compat.lastFragData);
|
||||
WRITE(p, " lowp vec4 destColor = subpassLoad(inputColor);\n");
|
||||
} else {
|
||||
_assert_msg_(false, "Need fetch destColor, but not a compatible language");
|
||||
}
|
||||
|
@ -3057,72 +3057,188 @@ void GPUCommon::DoBlockTransfer(u32 skipDrawReason) {
|
||||
|
||||
DEBUG_LOG(G3D, "Block transfer: %08x/%x -> %08x/%x, %ix%ix%i (%i,%i)->(%i,%i)", srcBasePtr, srcStride, dstBasePtr, dstStride, width, height, bpp, srcX, srcY, dstX, dstY);
|
||||
|
||||
if (!Memory::IsValidAddress(srcBasePtr)) {
|
||||
ERROR_LOG_REPORT(G3D, "BlockTransfer: Bad source transfer address %08x!", srcBasePtr);
|
||||
return;
|
||||
}
|
||||
// For VRAM, we wrap around when outside valid memory (mirrors still work.)
|
||||
if ((srcBasePtr & 0x04800000) == 0x04800000)
|
||||
srcBasePtr &= ~0x00800000;
|
||||
if ((dstBasePtr & 0x04800000) == 0x04800000)
|
||||
dstBasePtr &= ~0x00800000;
|
||||
|
||||
if (!Memory::IsValidAddress(dstBasePtr)) {
|
||||
ERROR_LOG_REPORT(G3D, "BlockTransfer: Bad destination transfer address %08x!", dstBasePtr);
|
||||
return;
|
||||
}
|
||||
// Use height less one to account for width, which can be greater or less than stride.
|
||||
const uint32_t src = srcBasePtr + (srcY * srcStride + srcX) * bpp;
|
||||
const uint32_t srcSize = (height - 1) * (srcStride + width) * bpp;
|
||||
const uint32_t dst = dstBasePtr + (dstY * dstStride + dstX) * bpp;
|
||||
const uint32_t dstSize = (height - 1) * (dstStride + width) * bpp;
|
||||
|
||||
// Check that the last address of both source and dest are valid addresses
|
||||
|
||||
u32 srcLastAddr = srcBasePtr + ((srcY + height - 1) * srcStride + (srcX + width - 1)) * bpp;
|
||||
u32 dstLastAddr = dstBasePtr + ((dstY + height - 1) * dstStride + (dstX + width - 1)) * bpp;
|
||||
|
||||
if (!Memory::IsValidAddress(srcLastAddr)) {
|
||||
ERROR_LOG_N_TIMES(bad_xfer_src, 5, G3D, "Bottom-right corner of source of %dx%d src=(%d, %d) block transfer from buffer at %08x is at an invalid address: %08x. Skipping.", width, height, srcX, srcY, srcBasePtr, srcLastAddr);
|
||||
return;
|
||||
}
|
||||
if (!Memory::IsValidAddress(dstLastAddr)) {
|
||||
ERROR_LOG_N_TIMES(bad_xfer_src, 5, G3D, "Bottom-right corner of destination of %dx%d dst=(%d, %d) block transfer to buffer at %08x is at an invalid address: %08x. Skipping.", width, height, dstX, dstY, dstBasePtr, srcLastAddr);
|
||||
return;
|
||||
}
|
||||
bool srcDstOverlap = src + srcSize > dst && dst + dstSize > src;
|
||||
bool srcValid = Memory::IsValidRange(src, srcSize);
|
||||
bool dstValid = Memory::IsValidRange(dst, dstSize);
|
||||
bool srcWraps = Memory::IsVRAMAddress(srcBasePtr) && !srcValid;
|
||||
bool dstWraps = Memory::IsVRAMAddress(dstBasePtr) && !dstValid;
|
||||
|
||||
// Tell the framebuffer manager to take action if possible. If it does the entire thing, let's just return.
|
||||
if (!framebufferManager_->NotifyBlockTransferBefore(dstBasePtr, dstStride, dstX, dstY, srcBasePtr, srcStride, srcX, srcY, width, height, bpp, skipDrawReason)) {
|
||||
if (!framebufferManager_ || !framebufferManager_->NotifyBlockTransferBefore(dstBasePtr, dstStride, dstX, dstY, srcBasePtr, srcStride, srcX, srcY, width, height, bpp, skipDrawReason)) {
|
||||
// Do the copy! (Hm, if we detect a drawn video frame (see below) then we could maybe skip this?)
|
||||
// Can use GetPointerUnchecked because we checked the addresses above. We could also avoid them
|
||||
// entirely by walking a couple of pointers...
|
||||
if (srcStride == dstStride && (u32)width == srcStride) {
|
||||
// Common case in God of War, let's do it all in one chunk.
|
||||
|
||||
// Simple case: just a straight copy, no overlap or wrapping.
|
||||
if (srcStride == dstStride && (u32)width == srcStride && !srcDstOverlap && srcValid && dstValid) {
|
||||
u32 srcLineStartAddr = srcBasePtr + (srcY * srcStride + srcX) * bpp;
|
||||
u32 dstLineStartAddr = dstBasePtr + (dstY * dstStride + dstX) * bpp;
|
||||
const u8 *src = Memory::GetPointerUnchecked(srcLineStartAddr);
|
||||
u8 *dst = Memory::GetPointerWriteUnchecked(dstLineStartAddr);
|
||||
memcpy(dst, src, width * height * bpp);
|
||||
GPURecord::NotifyMemcpy(dstLineStartAddr, srcLineStartAddr, width * height * bpp);
|
||||
} else {
|
||||
u32 bytesToCopy = width * height * bpp;
|
||||
|
||||
const u8 *srcp = Memory::GetPointer(srcLineStartAddr);
|
||||
u8 *dstp = Memory::GetPointerWrite(dstLineStartAddr);
|
||||
memcpy(dstp, srcp, bytesToCopy);
|
||||
|
||||
if (MemBlockInfoDetailed(bytesToCopy)) {
|
||||
const std::string tag = GetMemWriteTagAt("GPUBlockTransfer/", src, bytesToCopy);
|
||||
NotifyMemInfo(MemBlockFlags::READ, src, bytesToCopy, tag.c_str(), tag.size());
|
||||
NotifyMemInfo(MemBlockFlags::WRITE, dst, bytesToCopy, tag.c_str(), tag.size());
|
||||
}
|
||||
} else if ((srcDstOverlap || srcWraps || dstWraps) && (srcValid || srcWraps) && (dstValid || dstWraps)) {
|
||||
// This path means we have either src/dst overlap, OR one or both of src and dst wrap.
|
||||
// This should be uncommon so it's the slowest path.
|
||||
u32 bytesToCopy = width * bpp;
|
||||
static std::string tag;
|
||||
bool notifyDetail = MemBlockInfoDetailed(srcWraps || dstWraps ? 64 : bytesToCopy);
|
||||
bool notifyAll = !notifyDetail && MemBlockInfoDetailed(srcSize, dstSize);
|
||||
if (notifyDetail || notifyAll) {
|
||||
tag = GetMemWriteTagAt("GPUBlockTransfer/", src, srcSize);
|
||||
}
|
||||
|
||||
auto notifyingMemmove = [&](u32 d, u32 s, u32 sz) {
|
||||
const u8 *srcp = Memory::GetPointer(s);
|
||||
u8 *dstp = Memory::GetPointerWrite(d);
|
||||
memmove(dstp, srcp, sz);
|
||||
|
||||
if (notifyDetail) {
|
||||
NotifyMemInfo(MemBlockFlags::READ, s, sz, tag.c_str(), tag.size());
|
||||
NotifyMemInfo(MemBlockFlags::WRITE, d, sz, tag.c_str(), tag.size());
|
||||
}
|
||||
};
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
u32 srcLineStartAddr = srcBasePtr + ((y + srcY) * srcStride + srcX) * bpp;
|
||||
u32 dstLineStartAddr = dstBasePtr + ((y + dstY) * dstStride + dstX) * bpp;
|
||||
// If we already passed a wrap, we can use the quicker path.
|
||||
if ((srcLineStartAddr & 0x04800000) == 0x04800000)
|
||||
srcLineStartAddr &= ~0x00800000;
|
||||
if ((dstLineStartAddr & 0x04800000) == 0x04800000)
|
||||
dstLineStartAddr &= ~0x00800000;
|
||||
// These flags mean there's a wrap inside this line.
|
||||
bool srcLineWrap = !Memory::IsValidRange(srcLineStartAddr, bytesToCopy);
|
||||
bool dstLineWrap = !Memory::IsValidRange(dstLineStartAddr, bytesToCopy);
|
||||
|
||||
if (!srcLineWrap && !dstLineWrap) {
|
||||
const u8 *srcp = Memory::GetPointer(srcLineStartAddr);
|
||||
u8 *dstp = Memory::GetPointerWrite(dstLineStartAddr);
|
||||
for (u32 i = 0; i < bytesToCopy; i += 64) {
|
||||
u32 chunk = i + 64 > bytesToCopy ? bytesToCopy - i : 64;
|
||||
memmove(dstp + i, srcp + i, chunk);
|
||||
}
|
||||
|
||||
// If we're tracking detail, it's useful to have the gaps illustrated properly.
|
||||
if (notifyDetail) {
|
||||
NotifyMemInfo(MemBlockFlags::READ, srcLineStartAddr, bytesToCopy, tag.c_str(), tag.size());
|
||||
NotifyMemInfo(MemBlockFlags::WRITE, dstLineStartAddr, bytesToCopy, tag.c_str(), tag.size());
|
||||
}
|
||||
} else {
|
||||
// We can wrap at any point, so along with overlap this gets a bit complicated.
|
||||
// We're just going to do this the slow and easy way.
|
||||
u32 srcLinePos = srcLineStartAddr;
|
||||
u32 dstLinePos = dstLineStartAddr;
|
||||
for (u32 i = 0; i < bytesToCopy; i += 64) {
|
||||
u32 chunk = i + 64 > bytesToCopy ? bytesToCopy - i : 64;
|
||||
u32 srcValid = Memory::ValidSize(srcLinePos, chunk);
|
||||
u32 dstValid = Memory::ValidSize(dstLinePos, chunk);
|
||||
|
||||
// First chunk, for which both are valid.
|
||||
u32 bothSize = std::min(srcValid, dstValid);
|
||||
if (bothSize != 0)
|
||||
notifyingMemmove(dstLinePos, srcLinePos, bothSize);
|
||||
|
||||
// Now, whichever side has more valid (or the rest, if only one side must wrap.)
|
||||
u32 exclusiveSize = std::max(srcValid, dstValid) - bothSize;
|
||||
if (exclusiveSize != 0 && srcValid >= dstValid) {
|
||||
notifyingMemmove(PSP_GetVidMemBase(), srcLineStartAddr + bothSize, exclusiveSize);
|
||||
} else if (exclusiveSize != 0 && srcValid < dstValid) {
|
||||
notifyingMemmove(dstLineStartAddr + bothSize, PSP_GetVidMemBase(), exclusiveSize);
|
||||
}
|
||||
|
||||
// Finally, if both src and dst wrapped, that portion.
|
||||
u32 wrappedSize = chunk - bothSize - exclusiveSize;
|
||||
if (wrappedSize != 0 && srcValid >= dstValid) {
|
||||
notifyingMemmove(PSP_GetVidMemBase() + exclusiveSize, PSP_GetVidMemBase(), wrappedSize);
|
||||
} else if (wrappedSize != 0 && srcValid < dstValid) {
|
||||
notifyingMemmove(PSP_GetVidMemBase(), PSP_GetVidMemBase() + exclusiveSize, wrappedSize);
|
||||
}
|
||||
|
||||
srcLinePos += chunk;
|
||||
dstLinePos += chunk;
|
||||
if ((srcLinePos & 0x04800000) == 0x04800000)
|
||||
srcLinePos &= ~0x00800000;
|
||||
if ((dstLinePos & 0x04800000) == 0x04800000)
|
||||
dstLinePos &= ~0x00800000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (notifyAll) {
|
||||
if (srcWraps) {
|
||||
u32 validSize = Memory::ValidSize(src, srcSize);
|
||||
NotifyMemInfo(MemBlockFlags::READ, src, validSize, tag.c_str(), tag.size());
|
||||
NotifyMemInfo(MemBlockFlags::READ, PSP_GetVidMemBase(), srcSize - validSize, tag.c_str(), tag.size());
|
||||
} else {
|
||||
NotifyMemInfo(MemBlockFlags::READ, src, srcSize, tag.c_str(), tag.size());
|
||||
}
|
||||
if (dstWraps) {
|
||||
u32 validSize = Memory::ValidSize(dst, dstSize);
|
||||
NotifyMemInfo(MemBlockFlags::WRITE, dst, validSize, tag.c_str(), tag.size());
|
||||
NotifyMemInfo(MemBlockFlags::WRITE, PSP_GetVidMemBase(), dstSize - validSize, tag.c_str(), tag.size());
|
||||
} else {
|
||||
NotifyMemInfo(MemBlockFlags::WRITE, dst, dstSize, tag.c_str(), tag.size());
|
||||
}
|
||||
}
|
||||
} else if (srcValid && dstValid) {
|
||||
u32 bytesToCopy = width * bpp;
|
||||
static std::string tag;
|
||||
bool notifyDetail = MemBlockInfoDetailed(bytesToCopy);
|
||||
bool notifyAll = !notifyDetail && MemBlockInfoDetailed(srcSize, dstSize);
|
||||
if (notifyDetail || notifyAll) {
|
||||
tag = GetMemWriteTagAt("GPUBlockTransfer/", src, srcSize);
|
||||
}
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
u32 srcLineStartAddr = srcBasePtr + ((y + srcY) * srcStride + srcX) * bpp;
|
||||
u32 dstLineStartAddr = dstBasePtr + ((y + dstY) * dstStride + dstX) * bpp;
|
||||
|
||||
const u8 *src = Memory::GetPointerUnchecked(srcLineStartAddr);
|
||||
u8 *dst = Memory::GetPointerWriteUnchecked(dstLineStartAddr);
|
||||
memcpy(dst, src, width * bpp);
|
||||
GPURecord::NotifyMemcpy(dstLineStartAddr, srcLineStartAddr, width * bpp);
|
||||
const u8 *srcp = Memory::GetPointer(srcLineStartAddr);
|
||||
u8 *dstp = Memory::GetPointerWrite(dstLineStartAddr);
|
||||
memcpy(dstp, srcp, bytesToCopy);
|
||||
|
||||
// If we're tracking detail, it's useful to have the gaps illustrated properly.
|
||||
if (notifyDetail) {
|
||||
NotifyMemInfo(MemBlockFlags::READ, srcLineStartAddr, bytesToCopy, tag.c_str(), tag.size());
|
||||
NotifyMemInfo(MemBlockFlags::WRITE, dstLineStartAddr, bytesToCopy, tag.c_str(), tag.size());
|
||||
}
|
||||
}
|
||||
|
||||
if (notifyAll) {
|
||||
NotifyMemInfo(MemBlockFlags::READ, src, srcSize, tag.c_str(), tag.size());
|
||||
NotifyMemInfo(MemBlockFlags::WRITE, dst, dstSize, tag.c_str(), tag.size());
|
||||
}
|
||||
} else {
|
||||
// This seems to cause the GE to require a break/reset on a PSP.
|
||||
// TODO: Handle that and figure out which bytes are still copied?
|
||||
ERROR_LOG_REPORT_ONCE(invalidtransfer, G3D, "Block transfer invalid: %08x/%x -> %08x/%x, %ix%ix%i (%i,%i)->(%i,%i)", srcBasePtr, srcStride, dstBasePtr, dstStride, width, height, bpp, srcX, srcY, dstX, dstY);
|
||||
}
|
||||
|
||||
// Fixes Gran Turismo's funky text issue, since it overwrites the current texture.
|
||||
textureCache_->Invalidate(dstBasePtr + (dstY * dstStride + dstX) * bpp, height * dstStride * bpp, GPU_INVALIDATE_HINT);
|
||||
framebufferManager_->NotifyBlockTransferAfter(dstBasePtr, dstStride, dstX, dstY, srcBasePtr, srcStride, srcX, srcY, width, height, bpp, skipDrawReason);
|
||||
}
|
||||
|
||||
const uint32_t numBytes = width * height * bpp;
|
||||
const uint32_t srcSize = height * srcStride * bpp;
|
||||
const uint32_t dstSize = height * dstStride * bpp;
|
||||
// We do the check here on the number of bytes to avoid marking really tiny images.
|
||||
// Helps perf in GT menu which does insane amounts of these, one for each text character per frame.
|
||||
if (MemBlockInfoDetailed(numBytes, numBytes)) {
|
||||
const uint32_t src = srcBasePtr + (srcY * srcStride + srcX) * bpp;
|
||||
const uint32_t dst = dstBasePtr + (dstY * dstStride + dstX) * bpp;
|
||||
char tag[128];
|
||||
size_t tagSize = FormatMemWriteTagAt(tag, sizeof(tag), "GPUBlockTransfer/", src, srcSize);
|
||||
NotifyMemInfo(MemBlockFlags::READ, src, srcSize, tag, tagSize);
|
||||
NotifyMemInfo(MemBlockFlags::WRITE, dst, dstSize, tag, tagSize);
|
||||
if (framebufferManager_) {
|
||||
// Fixes Gran Turismo's funky text issue, since it overwrites the current texture.
|
||||
textureCache_->Invalidate(dstBasePtr + (dstY * dstStride + dstX) * bpp, height * dstStride * bpp, GPU_INVALIDATE_HINT);
|
||||
framebufferManager_->NotifyBlockTransferAfter(dstBasePtr, dstStride, dstX, dstY, srcBasePtr, srcStride, srcX, srcY, width, height, bpp, skipDrawReason);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Correct timing appears to be 1.9, but erring a bit low since some of our other timing is inaccurate.
|
||||
|
@ -301,6 +301,7 @@ protected:
|
||||
void UpdateState(GPURunState state);
|
||||
void FastLoadBoneMatrix(u32 target);
|
||||
void FlushImm();
|
||||
void DoBlockTransfer(u32 skipDrawReason);
|
||||
|
||||
// TODO: Unify this.
|
||||
virtual void FinishDeferred() {}
|
||||
@ -406,7 +407,6 @@ protected:
|
||||
|
||||
private:
|
||||
void CheckDepthUsage(VirtualFramebuffer *vfb);
|
||||
void DoBlockTransfer(u32 skipDrawReason);
|
||||
void DoExecuteCall(u32 target);
|
||||
void PopDLQueue();
|
||||
void CheckDrawSync();
|
||||
|
@ -50,16 +50,15 @@ inline float clip_dotprod(const ClipVertexData &vert, float A, float B, float C,
|
||||
}
|
||||
|
||||
inline void clip_interpolate(ClipVertexData &dest, float t, const ClipVertexData &a, const ClipVertexData &b) {
|
||||
if (different_signs(a.clippos.w, b.clippos.w)) {
|
||||
if (a.clippos.w < -1.0f || b.clippos.w < -1.0f) {
|
||||
dest.v.screenpos.x = 0x7FFFFFFF;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool outsideRange = false;
|
||||
dest.Lerp(t, a, b);
|
||||
dest.v.screenpos = TransformUnit::ClipToScreen(dest.clippos);
|
||||
dest.v.screenpos = TransformUnit::ClipToScreen(dest.clippos, &outsideRange);
|
||||
dest.v.clipw = dest.clippos.w;
|
||||
|
||||
// If the clipped coordinate is outside range, then we throw it away.
|
||||
// This prevents a lot of inversions that shouldn't be drawn.
|
||||
if (outsideRange)
|
||||
dest.v.screenpos.x = 0x7FFFFFFF;
|
||||
}
|
||||
|
||||
#define CLIP_POLY( PLANE_BIT, A, B, C, D ) \
|
||||
|
@ -29,6 +29,7 @@
|
||||
#include "Core/Core.h"
|
||||
#include "Core/Debugger/MemBlockInfo.h"
|
||||
#include "Core/MemMap.h"
|
||||
#include "Core/MemMapHelpers.h"
|
||||
#include "Core/HLE/sceKernelInterrupt.h"
|
||||
#include "Core/HLE/sceGe.h"
|
||||
#include "Core/MIPS/MIPS.h"
|
||||
@ -792,68 +793,21 @@ void SoftGPU::Execute_BlockTransferStart(u32 op, u32 diff) {
|
||||
|
||||
int bpp = gstate.getTransferBpp();
|
||||
|
||||
// Use height less one to account for width, which can be greater or less than stride.
|
||||
const uint32_t src = srcBasePtr + (srcY * srcStride + srcX) * bpp;
|
||||
const uint32_t srcSize = height * srcStride * bpp;
|
||||
const uint32_t srcSize = (height - 1) * (srcStride + width) * bpp;
|
||||
const uint32_t dst = dstBasePtr + (dstY * dstStride + dstX) * bpp;
|
||||
const uint32_t dstSize = height * dstStride * bpp;
|
||||
const uint32_t dstSize = (height - 1) * (dstStride + width) * bpp;
|
||||
|
||||
// Need to flush both source and target, so we overwrite properly.
|
||||
drawEngine_->transformUnit.FlushIfOverlap("blockxfer", false, src, srcStride, width * bpp, height);
|
||||
drawEngine_->transformUnit.FlushIfOverlap("blockxfer", true, dst, dstStride, width * bpp, height);
|
||||
|
||||
DEBUG_LOG(G3D, "Block transfer: %08x/%x -> %08x/%x, %ix%ix%i (%i,%i)->(%i,%i)", srcBasePtr, srcStride, dstBasePtr, dstStride, width, height, bpp, srcX, srcY, dstX, dstY);
|
||||
|
||||
if (srcStride == dstStride && (u32)width == srcStride) {
|
||||
u32 srcLineStartAddr = srcBasePtr + (srcY * srcStride + srcX) * bpp;
|
||||
u32 dstLineStartAddr = dstBasePtr + (dstY * dstStride + dstX) * bpp;
|
||||
|
||||
u32 bytesToCopy = width * height * bpp;
|
||||
|
||||
if (!Memory::IsValidRange(srcLineStartAddr, bytesToCopy)) {
|
||||
// What should we do here? Memset zeroes to the dest instead?
|
||||
return;
|
||||
}
|
||||
if (!Memory::IsValidRange(dstLineStartAddr, bytesToCopy)) {
|
||||
// What should we do here? Just not do the write, or partial write if
|
||||
// some part is in-range?
|
||||
return;
|
||||
}
|
||||
|
||||
const u8 *srcp = Memory::GetPointer(srcLineStartAddr);
|
||||
u8 *dstp = Memory::GetPointerWrite(dstLineStartAddr);
|
||||
memcpy(dstp, srcp, bytesToCopy);
|
||||
GPURecord::NotifyMemcpy(dstLineStartAddr, srcLineStartAddr, bytesToCopy);
|
||||
if (Memory::IsValidRange(src, srcSize) && Memory::IsValidRange(dst, dstSize)) {
|
||||
drawEngine_->transformUnit.FlushIfOverlap("blockxfer", false, src, srcStride, width * bpp, height);
|
||||
drawEngine_->transformUnit.FlushIfOverlap("blockxfer", true, dst, dstStride, width * bpp, height);
|
||||
} else {
|
||||
for (int y = 0; y < height; y++) {
|
||||
u32 srcLineStartAddr = srcBasePtr + ((y + srcY) * srcStride + srcX) * bpp;
|
||||
u32 dstLineStartAddr = dstBasePtr + ((y + dstY) * dstStride + dstX) * bpp;
|
||||
|
||||
u32 bytesToCopy = width * bpp;
|
||||
if (!Memory::IsValidRange(srcLineStartAddr, bytesToCopy)) {
|
||||
// What should we do here? Due to the y loop, in this case we might have
|
||||
// performed a partial copy. Probably fine.
|
||||
break;
|
||||
}
|
||||
if (!Memory::IsValidRange(dstLineStartAddr, bytesToCopy)) {
|
||||
// What should we do here? Due to the y loop, in this case we might have
|
||||
// performed a partial copy. Probably fine.
|
||||
break;
|
||||
}
|
||||
const u8 *srcp = Memory::GetPointer(srcLineStartAddr);
|
||||
u8 *dstp = Memory::GetPointerWrite(dstLineStartAddr);
|
||||
memcpy(dstp, srcp, width * bpp);
|
||||
GPURecord::NotifyMemcpy(dstLineStartAddr, srcLineStartAddr, width * bpp);
|
||||
}
|
||||
drawEngine_->transformUnit.Flush("blockxfer_wrap");
|
||||
}
|
||||
|
||||
if (MemBlockInfoDetailed(srcSize, dstSize)) {
|
||||
const std::string tag = GetMemWriteTagAt("GPUBlockTransfer/", src, srcSize);
|
||||
NotifyMemInfo(MemBlockFlags::READ, src, srcSize, tag.c_str(), tag.size());
|
||||
NotifyMemInfo(MemBlockFlags::WRITE, dst, dstSize, tag.c_str(), tag.size());
|
||||
}
|
||||
|
||||
// TODO: Correct timing appears to be 1.9, but erring a bit low since some of our other timing is inaccurate.
|
||||
cyclesExecuted += ((height * width * bpp) * 16) / 10;
|
||||
DoBlockTransfer(gstate_c.skipDrawReason);
|
||||
|
||||
// Could theoretically dirty the framebuffer.
|
||||
MarkDirty(dst, dstSize, SoftGPUVRAMDirty::DIRTY | SoftGPUVRAMDirty::REALLY_DIRTY);
|
||||
|
@ -162,7 +162,7 @@ ClipCoords TransformUnit::ViewToClip(const ViewCoords &coords) {
|
||||
return Vec3ByMatrix44(coords, gstate.projMatrix);
|
||||
}
|
||||
|
||||
template <bool depthClamp, bool writeOutsideFlag>
|
||||
template <bool depthClamp, bool alwaysCheckRange>
|
||||
static ScreenCoords ClipToScreenInternal(Vec3f scaled, const ClipCoords &coords, bool *outside_range_flag) {
|
||||
ScreenCoords ret;
|
||||
|
||||
@ -173,7 +173,7 @@ static ScreenCoords ClipToScreenInternal(Vec3f scaled, const ClipCoords &coords,
|
||||
// This matches hardware tests - depth is clamped when this flag is on.
|
||||
if (depthClamp) {
|
||||
// Note: if the depth is clipped (z/w <= -1.0), the outside_range_flag should NOT be set, even for x and y.
|
||||
if (writeOutsideFlag && coords.z > -coords.w && (scaled.x >= SCREEN_BOUND || scaled.y >= SCREEN_BOUND || scaled.x < 0 || scaled.y < 0)) {
|
||||
if ((alwaysCheckRange || coords.z > -coords.w) && (scaled.x >= SCREEN_BOUND || scaled.y >= SCREEN_BOUND || scaled.x < 0 || scaled.y < 0)) {
|
||||
*outside_range_flag = true;
|
||||
}
|
||||
|
||||
@ -181,7 +181,7 @@ static ScreenCoords ClipToScreenInternal(Vec3f scaled, const ClipCoords &coords,
|
||||
scaled.z = 0.f;
|
||||
else if (scaled.z > 65535.0f)
|
||||
scaled.z = 65535.0f;
|
||||
} else if (writeOutsideFlag && (scaled.x > SCREEN_BOUND || scaled.y >= SCREEN_BOUND || scaled.x < 0 || scaled.y < 0)) {
|
||||
} else if (scaled.x > SCREEN_BOUND || scaled.y >= SCREEN_BOUND || scaled.x < 0 || scaled.y < 0) {
|
||||
*outside_range_flag = true;
|
||||
}
|
||||
|
||||
@ -209,17 +209,13 @@ static inline ScreenCoords ClipToScreenInternal(const ClipCoords &coords, bool *
|
||||
float z = coords.z * zScale / coords.w + zCenter;
|
||||
|
||||
if (gstate.isDepthClampEnabled()) {
|
||||
if (outside_range_flag)
|
||||
return ClipToScreenInternal<true, true>(Vec3f(x, y, z), coords, outside_range_flag);
|
||||
return ClipToScreenInternal<true, false>(Vec3f(x, y, z), coords, outside_range_flag);
|
||||
return ClipToScreenInternal<true, true>(Vec3f(x, y, z), coords, outside_range_flag);
|
||||
}
|
||||
if (outside_range_flag)
|
||||
return ClipToScreenInternal<false, true>(Vec3f(x, y, z), coords, outside_range_flag);
|
||||
return ClipToScreenInternal<false, false>(Vec3f(x, y, z), coords, outside_range_flag);
|
||||
return ClipToScreenInternal<false, true>(Vec3f(x, y, z), coords, outside_range_flag);
|
||||
}
|
||||
|
||||
ScreenCoords TransformUnit::ClipToScreen(const ClipCoords &coords) {
|
||||
return ClipToScreenInternal(coords, nullptr);
|
||||
ScreenCoords TransformUnit::ClipToScreen(const ClipCoords &coords, bool *outsideRangeFlag) {
|
||||
return ClipToScreenInternal(coords, outsideRangeFlag);
|
||||
}
|
||||
|
||||
ScreenCoords TransformUnit::DrawingToScreen(const DrawingCoords &coords, u16 z) {
|
||||
@ -317,9 +313,9 @@ void ComputeTransformState(TransformState *state, const VertexReader &vreader) {
|
||||
}
|
||||
|
||||
if (gstate.isDepthClampEnabled())
|
||||
state->roundToScreen = &ClipToScreenInternal<true, true>;
|
||||
state->roundToScreen = &ClipToScreenInternal<true, false>;
|
||||
else
|
||||
state->roundToScreen = &ClipToScreenInternal<false, true>;
|
||||
state->roundToScreen = &ClipToScreenInternal<false, false>;
|
||||
}
|
||||
|
||||
ClipVertexData TransformUnit::ReadVertex(VertexReader &vreader, const TransformState &state) {
|
||||
@ -977,7 +973,8 @@ bool TransformUnit::GetCurrentSimpleVertices(int count, std::vector<GPUDebugVert
|
||||
vertices[i].z = vert.pos.z;
|
||||
} else {
|
||||
Vec4f clipPos = Vec3ByMatrix44(vert.pos, worldviewproj);
|
||||
ScreenCoords screenPos = ClipToScreen(clipPos);
|
||||
bool outsideRangeFlag;
|
||||
ScreenCoords screenPos = ClipToScreen(clipPos, &outsideRangeFlag);
|
||||
float z = clipPos.z * zScale / clipPos.w + zCenter;
|
||||
|
||||
if (gstate.vertType & GE_VTYPE_TC_MASK) {
|
||||
|
@ -121,7 +121,7 @@ public:
|
||||
static WorldCoords ModelToWorld(const ModelCoords& coords);
|
||||
static ViewCoords WorldToView(const WorldCoords& coords);
|
||||
static ClipCoords ViewToClip(const ViewCoords& coords);
|
||||
static ScreenCoords ClipToScreen(const ClipCoords& coords);
|
||||
static ScreenCoords ClipToScreen(const ClipCoords &coords, bool *outsideRangeFlag);
|
||||
static inline DrawingCoords ScreenToDrawing(int x, int y) {
|
||||
DrawingCoords ret;
|
||||
// When offset > coord, this is negative and force-scissors.
|
||||
|
@ -63,15 +63,14 @@ public:
|
||||
bool Touch(const TouchInput &touch) {
|
||||
int mode = mode_ ? mode_->GetSelection() : 0;
|
||||
|
||||
const Bounds &screenBounds = bounds_;
|
||||
if ((touch.flags & TOUCH_MOVE) != 0 && dragging_) {
|
||||
float relativeTouchX = touch.x - startX_;
|
||||
float relativeTouchY = touch.y - startY_;
|
||||
|
||||
switch (mode) {
|
||||
case MODE_MOVE:
|
||||
g_Config.fDisplayOffsetX = clamp_value(startDisplayOffsetX_ + relativeTouchX / screenBounds.w, 0.0f, 1.0f);
|
||||
g_Config.fDisplayOffsetY = clamp_value(startDisplayOffsetY_ + relativeTouchY / screenBounds.h, 0.0f, 1.0f);
|
||||
g_Config.fDisplayOffsetX = clamp_value(startDisplayOffsetX_ + relativeTouchX / bounds_.w, 0.0f, 1.0f);
|
||||
g_Config.fDisplayOffsetY = clamp_value(startDisplayOffsetY_ + relativeTouchY / bounds_.h, 0.0f, 1.0f);
|
||||
break;
|
||||
case MODE_RESIZE:
|
||||
{
|
||||
@ -84,12 +83,18 @@ public:
|
||||
}
|
||||
|
||||
if ((touch.flags & TOUCH_DOWN) != 0 && !dragging_) {
|
||||
dragging_ = true;
|
||||
startX_ = touch.x;
|
||||
startY_ = touch.y;
|
||||
startDisplayOffsetX_ = g_Config.fDisplayOffsetX;
|
||||
startDisplayOffsetY_ = g_Config.fDisplayOffsetY;
|
||||
startScale_ = g_Config.fDisplayScale;
|
||||
// Check that we're in the central 80% of the screen.
|
||||
// If outside, it may be a drag from displaying the back button on phones
|
||||
// where you have to drag from the side, etc.
|
||||
if (touch.x >= bounds_.w * 0.1f && touch.x <= bounds_.w * 0.9f &&
|
||||
touch.y >= bounds_.h * 0.1f && touch.y <= bounds_.h * 0.9f) {
|
||||
dragging_ = true;
|
||||
startX_ = touch.x;
|
||||
startY_ = touch.y;
|
||||
startDisplayOffsetX_ = g_Config.fDisplayOffsetX;
|
||||
startDisplayOffsetY_ = g_Config.fDisplayOffsetY;
|
||||
startScale_ = g_Config.fDisplayScale;
|
||||
}
|
||||
}
|
||||
|
||||
if ((touch.flags & TOUCH_UP) != 0 && dragging_) {
|
||||
@ -208,7 +213,7 @@ void DisplayLayoutScreen::CreateViews() {
|
||||
auto stretch = new CheckBox(&g_Config.bDisplayStretch, gr->T("Stretch"));
|
||||
leftColumn->Add(stretch);
|
||||
|
||||
PopupSliderChoiceFloat *aspectRatio = new PopupSliderChoiceFloat(&g_Config.fDisplayAspectRatio, 0.5f, 2.0f, di->T("Aspect Ratio"), screenManager());
|
||||
PopupSliderChoiceFloat *aspectRatio = new PopupSliderChoiceFloat(&g_Config.fDisplayAspectRatio, 0.5f, 2.0f, gr->T("Aspect Ratio"), screenManager());
|
||||
leftColumn->Add(aspectRatio);
|
||||
aspectRatio->SetDisabledPtr(&g_Config.bDisplayStretch);
|
||||
aspectRatio->SetHasDropShadow(false);
|
||||
|
@ -1793,7 +1793,7 @@ void DeveloperToolsScreen::CreateViews() {
|
||||
list->Add(new CheckBox(&g_Config.bStereoRendering, gr->T("Stereo rendering")));
|
||||
std::vector<std::string> stereoShaderNames;
|
||||
|
||||
ChoiceWithValueDisplay *stereoShaderChoice = list->Add(new ChoiceWithValueDisplay(&g_Config.sStereoToMonoShader, "Stereo display shader", &PostShaderTranslateName));
|
||||
ChoiceWithValueDisplay *stereoShaderChoice = list->Add(new ChoiceWithValueDisplay(&g_Config.sStereoToMonoShader, gr->T("Stereo display shader"), &PostShaderTranslateName));
|
||||
stereoShaderChoice->SetEnabledFunc(enableStereo);
|
||||
stereoShaderChoice->OnClick.Add([=](EventParams &e) {
|
||||
auto gr = GetI18NCategory("Graphics");
|
||||
|
@ -82,7 +82,7 @@ Portrait = Портретная
|
||||
Portrait Reversed = Портретная (перевернутая)
|
||||
PSP Action Buttons = Кнопки действий PSP
|
||||
Raw input = Прямой ввод
|
||||
Repeat mode = Repeat mode
|
||||
Repeat mode = Режим повтора
|
||||
Reset to defaults = По умолчанию
|
||||
Screen aligned to ground = Экран повёрнут к земле
|
||||
Screen at right angle to ground = Экран повёрнут под прямым углом к земле
|
||||
@ -240,7 +240,7 @@ FPU = FPU
|
||||
Framedump tests = Тест дампов кадров
|
||||
Frame Profiler = Профайлер кадров
|
||||
GPU Driver Test = Проверка драйвера ГП
|
||||
GPU log profiler = GPU log profiler
|
||||
GPU log profiler = Профилировщик логов ГП
|
||||
GPU Profile = Профиль ГП
|
||||
Jit Compare = Сравнение с JIT
|
||||
JIT debug tools = Инструменты отладки JIT
|
||||
@ -456,7 +456,7 @@ Aggressive = Принудительно
|
||||
Alternative Speed = Другая скорость (в %, 0 = без ограничений)
|
||||
Alternative Speed 2 = Другая скорость 2 (в %, 0 = без ограничений)
|
||||
Anisotropic Filtering = Анизотропная фильтрация
|
||||
Aspect Ratio = Aspect Ratio
|
||||
Aspect Ratio = Соотношение сторон
|
||||
Auto = Авто
|
||||
Auto (1:1) = Авто (1:1)
|
||||
Auto (same as Rendering) = Авто (как разрешение рендеринга)
|
||||
@ -488,14 +488,13 @@ Disabled = Отключено
|
||||
Display Layout && Effects = Редактор расположения экрана
|
||||
Display Resolution (HW scaler) = Разрешение экрана (аппаратное)
|
||||
Enable Cardboard VR = Включить Cardboard VR
|
||||
#Features = Возможности
|
||||
FPS = FPS
|
||||
Frame Rate Control = Управление частотой кадров
|
||||
Frame Skipping = Пропуск кадров
|
||||
Frame Skipping Type = Тип пропуска кадров
|
||||
FullScreen = Полноэкранный режим
|
||||
Geometry shader culling = Geometry shader culling
|
||||
GPUReadbackRequired = Warning: This game requires "Skip GPU Readbacks" to be set to Off.
|
||||
Geometry shader culling = Вызов геометрических шейдеров
|
||||
GPUReadbackRequired = Внимание: Для данной игры нужно отключить настройку "Пропускать чтение данных ГП".
|
||||
Hack Settings = Параметры хаков (могут вызывать глюки)
|
||||
Hardware Tessellation = Аппаратная тесселяция
|
||||
Hardware Transform = Аппаратное преобразование
|
||||
@ -542,20 +541,20 @@ Safe = Безопасно
|
||||
Screen Scaling Filter = Фильтр масштабирования экрана
|
||||
Show Debug Statistics = Отображать отладочную информацию
|
||||
Show FPS Counter = Показывать счетчик FPS
|
||||
Skip GPU Readbacks = Skip GPU Readbacks
|
||||
Skip GPU Readbacks = Пропускать чтение данных ГП
|
||||
Software Rendering = Программный рендеринг (медленно)
|
||||
Software Skinning = Программная заливка
|
||||
SoftwareSkinning Tip = Объединяет вызовы отрисовки моделей с заливкой на ЦП, быстрее во многих играх
|
||||
Speed = Скорость
|
||||
Speed Hacks = Speed Hacks (могут вызывать глюки)
|
||||
Stereo display shader = Stereo display shader
|
||||
Stereo rendering = Stereo rendering
|
||||
Stereo display shader = Шейдер стереодисплея
|
||||
Stereo rendering = Стереорендеринг
|
||||
Stretch = Растягивание
|
||||
Texture Filter = Текстурный фильтр
|
||||
Texture Filtering = Фильтрация текстур
|
||||
Texture Scaling = Масштабирование текстур
|
||||
Texture Shader = Текстурный шейдер
|
||||
Turn off Hardware Tessellation - unsupported = Выключите "аппаратную тесселяцию": не поддерживается
|
||||
Turn off Hardware Tessellation - unsupported = Выключите настройку "Аппаратная тесселяция": не поддерживается
|
||||
Unlimited = Без ограничений
|
||||
Up to 1 = До 1
|
||||
Up to 2 = До 2
|
||||
@ -581,8 +580,8 @@ Zip archive corrupt = ZIP-архив повреждён
|
||||
Zip file does not contain PSP software = В ZIP-файле отсутсвует ПО для PSP
|
||||
|
||||
[KeyMapping]
|
||||
Autoconfigure = Авто конфиг
|
||||
Autoconfigure for device = Авто конфиг для устройства
|
||||
Autoconfigure = Автоконфиг
|
||||
Autoconfigure for device = Автоконфиг для устройства
|
||||
Bind All = Настроить всё
|
||||
Clear All = Очистить все
|
||||
Default All = По умолчанию
|
||||
@ -722,8 +721,8 @@ Center Left = В центре слева
|
||||
Center Right = В центре справа
|
||||
Change Mac Address = Изменить MAC-адрес
|
||||
Change proAdhocServer Address = Изменить IP-адрес ad-hoc сервера (localhost = множество экземпляров)
|
||||
ChangeMacSaveConfirm = Generate a new MAC address?
|
||||
ChangeMacSaveWarning = Some games verify the MAC address when loading savedata, so this may break old saves.
|
||||
ChangeMacSaveConfirm = Сгенерировать новый MAC-адрес?
|
||||
ChangeMacSaveWarning = Некоторые игры проверяют MAC-адрес при загрузке сохранений, из-за этого старые сохранения могут быть испорчены.
|
||||
Chat = Чат
|
||||
Chat Button Position = Позиция кнопки чата
|
||||
Chat Here = Чат здесь
|
||||
@ -747,7 +746,7 @@ Failed to Bind Localhost IP = Не удалось привязать адрес
|
||||
Failed to Bind Port = Не удалось привязать порт
|
||||
Failed to connect to Adhoc Server = Не удалось подключиться к ad-hoc серверу
|
||||
Forced First Connect = Принудительное первое подключение (быстрое подключение)
|
||||
GM: Data from Unknown Port = GM: Данные от неизвестного порта
|
||||
GM: Data from Unknown Port = GM: Данные с неизвестного порта
|
||||
Hostname = Имя хоста
|
||||
Invalid IP or hostname = Некорректный IP или имя хоста
|
||||
Minimum Timeout = Минимальный таймаут (задержка в мс, 0 = по умолчанию)
|
||||
@ -804,9 +803,9 @@ Black border = Черные рамки
|
||||
Bloom = Свечение
|
||||
Brightness = Яркость
|
||||
Cartoon = Мультипликация
|
||||
CatmullRom = Бикубический (Catmull-Rom) Апскейлер
|
||||
CatmullRom = Бикубический (Catmull-Rom) апскейлер
|
||||
ColorCorrection = Цветокоррекция
|
||||
ColorPreservation = Color preservation
|
||||
ColorPreservation = Сохранение цвета
|
||||
Contrast = Контрастность
|
||||
CRT = ЭЛТ-развертка
|
||||
FXAA = Сглаживание FXAA
|
||||
@ -815,13 +814,13 @@ Grayscale = Оттенки серого
|
||||
GreenLevel = GreenLevel
|
||||
Intensity = Интенсивность
|
||||
InverseColors = Инвертированные цвета
|
||||
MitchellNetravali = Бикубический (Mitchell-Netravali) Апскейлер
|
||||
MitchellNetravali = Бикубический (Mitchell-Netravali) апскейлер
|
||||
Natural = Естественные цвета
|
||||
NaturalA = Естественные цвета (без размытия)
|
||||
Off = Выключена
|
||||
Power = Сила
|
||||
PSPColor = Цвета PSP
|
||||
RedBlue = Red/Blue glasses
|
||||
RedBlue = Красно-синие очки
|
||||
Saturation = Насыщенность
|
||||
Scanlines = Строки развертки (ЭЛТ)
|
||||
Sharpen = Резкость
|
||||
@ -1043,10 +1042,10 @@ CPU Name = Название
|
||||
D3DCompiler Version = Версия D3DCompiler
|
||||
Debug = Отладочная
|
||||
Debugger Present = Отладчик присутствует
|
||||
#Depth buffer format = Формат буфера глубины
|
||||
Depth buffer format = Формат буфера глубины
|
||||
Device Info = Устройство
|
||||
Directories = Директории
|
||||
Display Color Formats = Display Color Formats
|
||||
Display Color Formats = Форматы цветов дисплея
|
||||
Display Information = Информация о дисплее
|
||||
Driver bugs = Ошибки драйвера
|
||||
Driver Version = Версия драйвера
|
||||
@ -1096,7 +1095,7 @@ Cache ISO in RAM = Кэшировать ISO в ОЗУ
|
||||
Change CPU Clock = Эмулируемая частота ЦП PSP (нестабильно)
|
||||
Color Saturation = Насыщенность
|
||||
Color Tint = Оттенок цвета
|
||||
Game crashed = Game crashed
|
||||
Game crashed = Игра вылетела
|
||||
Language = Язык
|
||||
Memory Stick folder = Изменить папку с картой памяти
|
||||
Memory Stick size = Изменить размер карты памяти (Гб)
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit a886587cc29ffadc93f5533a0f8dc329ae355b9a
|
||||
Subproject commit 46065027500cd781ce7c15c051c8b0c4751ad1fa
|
Loading…
Reference in New Issue
Block a user