mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-11 13:36:24 +02:00
Merge branch 'hrydgard:master' into compat_openxr_gta
This commit is contained in:
40 files changed
+723
-165
No files matched your search
@@ -105,6 +105,7 @@ public:
|
||||
void InvalidateCachedState() override;
|
||||
|
||||
void BindTextures(int start, int count, Texture **textures) override;
|
||||
void BindNativeTexture(int index, void *nativeTexture) override;
|
||||
void BindSamplerStates(int start, int count, SamplerState **states) override;
|
||||
void BindVertexBuffers(int start, int count, Buffer **buffers, const int *offsets) override;
|
||||
void BindIndexBuffer(Buffer *indexBuffer, int offset) override;
|
||||
@@ -471,6 +472,7 @@ static DXGI_FORMAT dataFormatToD3D11(DataFormat format) {
|
||||
case DataFormat::R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM_SRGB;
|
||||
case DataFormat::B8G8R8A8_UNORM: return DXGI_FORMAT_B8G8R8A8_UNORM;
|
||||
case DataFormat::B8G8R8A8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM_SRGB;
|
||||
case DataFormat::R16_UNORM: return DXGI_FORMAT_R16_UNORM;
|
||||
case DataFormat::R16_FLOAT: return DXGI_FORMAT_R16_FLOAT;
|
||||
case DataFormat::R16G16_FLOAT: return DXGI_FORMAT_R16G16_FLOAT;
|
||||
case DataFormat::R16G16B16A16_FLOAT: return DXGI_FORMAT_R16G16B16A16_FLOAT;
|
||||
@@ -1388,6 +1390,12 @@ void D3D11DrawContext::BindTextures(int start, int count, Texture **textures) {
|
||||
context_->PSSetShaderResources(start, count, views);
|
||||
}
|
||||
|
||||
void D3D11DrawContext::BindNativeTexture(int index, void *nativeTexture) {
|
||||
// Collect the resource views from the textures.
|
||||
ID3D11ShaderResourceView *view = (ID3D11ShaderResourceView *)nativeTexture;
|
||||
context_->PSSetShaderResources(index, 1, &view);
|
||||
}
|
||||
|
||||
void D3D11DrawContext::BindSamplerStates(int start, int count, SamplerState **states) {
|
||||
ID3D11SamplerState *samplers[MAX_BOUND_TEXTURES];
|
||||
_assert_(start + count <= ARRAY_SIZE(samplers));
|
||||
|
||||
@@ -114,6 +114,7 @@ static const D3DSTENCILOP stencilOpToD3D9[] = {
|
||||
|
||||
D3DFORMAT FormatToD3DFMT(DataFormat fmt) {
|
||||
switch (fmt) {
|
||||
case DataFormat::R16_UNORM: return D3DFMT_L16; // closest match, should be a fine substitution if we ignore channels except R.
|
||||
case DataFormat::R8G8B8A8_UNORM: return D3DFMT_A8R8G8B8;
|
||||
case DataFormat::B8G8R8A8_UNORM: return D3DFMT_A8R8G8B8;
|
||||
case DataFormat::R4G4B4A4_UNORM_PACK16: return D3DFMT_A4R4G4B4; // emulated
|
||||
@@ -442,6 +443,17 @@ void D3D9Texture::SetImageData(int x, int y, int z, int width, int height, int d
|
||||
if (data != rect.pBits)
|
||||
memcpy(dest, source, sizeof(uint32_t) * width);
|
||||
break;
|
||||
|
||||
case DataFormat::R8_UNORM:
|
||||
if (data != rect.pBits)
|
||||
memcpy(dest, source, width);
|
||||
break;
|
||||
|
||||
case DataFormat::R16_UNORM:
|
||||
if (data != rect.pBits)
|
||||
memcpy(dest, source, sizeof(uint16_t) * width);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Unhandled data format copy.
|
||||
DebugBreak();
|
||||
@@ -520,6 +532,8 @@ public:
|
||||
void GetFramebufferDimensions(Framebuffer *fbo, int *w, int *h) override;
|
||||
|
||||
void BindTextures(int start, int count, Texture **textures) override;
|
||||
void BindNativeTexture(int index, void *nativeTexture) override;
|
||||
|
||||
void BindSamplerStates(int start, int count, SamplerState **states) override {
|
||||
_assert_(start + count <= MAX_BOUND_TEXTURES);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
@@ -813,6 +827,11 @@ void D3D9Context::BindTextures(int start, int count, Texture **textures) {
|
||||
}
|
||||
}
|
||||
|
||||
void D3D9Context::BindNativeTexture(int index, void *nativeTexture) {
|
||||
LPDIRECT3DTEXTURE9 texture = (LPDIRECT3DTEXTURE9)nativeTexture;
|
||||
device_->SetTexture(index, texture);
|
||||
}
|
||||
|
||||
void D3D9Context::EndFrame() {
|
||||
curPipeline_ = nullptr;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ enum class DataFormat : uint8_t {
|
||||
A1R5G5B5_UNORM_PACK16, // A1 in the UPPER bit.
|
||||
A1B5G5R5_UNORM_PACK16, // A1 in the UPPER bit. OpenGL-only.
|
||||
|
||||
R16_UNORM,
|
||||
|
||||
R16_FLOAT,
|
||||
R16G16_FLOAT,
|
||||
R16G16B16A16_FLOAT,
|
||||
|
||||
@@ -4,9 +4,23 @@
|
||||
namespace Draw {
|
||||
|
||||
// TODO: Also output storage format (GL_RGBA8 etc) for modern GL usage.
|
||||
bool Thin3DFormatToFormatAndType(DataFormat fmt, GLuint &internalFormat, GLuint &format, GLuint &type, int &alignment) {
|
||||
bool Thin3DFormatToGLFormatAndType(DataFormat fmt, GLuint &internalFormat, GLuint &format, GLuint &type, int &alignment) {
|
||||
alignment = 4;
|
||||
switch (fmt) {
|
||||
case DataFormat::R16_UNORM:
|
||||
internalFormat = GL_RGBA;
|
||||
format = GL_RED;
|
||||
type = GL_UNSIGNED_SHORT;
|
||||
alignment = 2;
|
||||
break;
|
||||
|
||||
case DataFormat::R8_UNORM:
|
||||
internalFormat = GL_RGBA;
|
||||
format = GL_RED;
|
||||
type = GL_UNSIGNED_BYTE;
|
||||
alignment = 1;
|
||||
break;
|
||||
|
||||
case DataFormat::R8G8B8A8_UNORM:
|
||||
internalFormat = GL_RGBA;
|
||||
format = GL_RGBA;
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
|
||||
namespace Draw {
|
||||
|
||||
bool Thin3DFormatToFormatAndType(DataFormat fmt, GLuint &internalFormat, GLuint &format, GLuint &type, int &alignment);
|
||||
bool Thin3DFormatToGLFormatAndType(DataFormat fmt, GLuint &internalFormat, GLuint &format, GLuint &type, int &alignment);
|
||||
|
||||
}
|
||||
@@ -385,7 +385,7 @@ void GLQueueRunner::RunInitSteps(const std::vector<GLRInitStep> &steps, bool ski
|
||||
|
||||
GLenum internalFormat, format, type;
|
||||
int alignment;
|
||||
Thin3DFormatToFormatAndType(step.texture_image.format, internalFormat, format, type, alignment);
|
||||
Thin3DFormatToGLFormatAndType(step.texture_image.format, internalFormat, format, type, alignment);
|
||||
if (step.texture_image.depth == 1) {
|
||||
glTexImage2D(tex->target,
|
||||
step.texture_image.level, internalFormat,
|
||||
@@ -1283,7 +1283,7 @@ void GLQueueRunner::PerformRenderPass(const GLRStep &step, bool first, bool last
|
||||
// For things to show in RenderDoc, need to split into glTexImage2D(..., nullptr) and glTexSubImage.
|
||||
GLuint internalFormat, format, type;
|
||||
int alignment;
|
||||
Thin3DFormatToFormatAndType(c.texture_subimage.format, internalFormat, format, type, alignment);
|
||||
Thin3DFormatToGLFormatAndType(c.texture_subimage.format, internalFormat, format, type, alignment);
|
||||
glTexSubImage2D(tex->target, c.texture_subimage.level, c.texture_subimage.x, c.texture_subimage.y, c.texture_subimage.width, c.texture_subimage.height, format, type, c.texture_subimage.data);
|
||||
if (c.texture_subimage.allocType == GLRAllocType::ALIGNED) {
|
||||
FreeAlignedMemory(c.texture_subimage.data);
|
||||
|
||||
@@ -235,11 +235,29 @@ bool GLRenderManager::ThreadFrame() {
|
||||
INFO_LOG(G3D, "Running first frame (%d)", threadFrame_);
|
||||
firstFrame = false;
|
||||
}
|
||||
|
||||
// Start of an OpenXR frame. This updates user's head pose and VR timestamps.
|
||||
// For fluent rendering, delay between StartVRRender and FinishVRRender must be very short.
|
||||
if (IsVRBuild() && !vrRenderStarted) {
|
||||
if (StartVRRender()) {
|
||||
vrRenderStarted = true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Render the scene.
|
||||
Run(threadFrame_);
|
||||
|
||||
VLOG("PULL: Finished frame %d", threadFrame_);
|
||||
} while (!nextFrame);
|
||||
|
||||
// Post OpenXR frame on a screen.
|
||||
if (IsVRBuild() && vrRenderStarted) {
|
||||
FinishVRRender();
|
||||
vrRenderStarted = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -579,17 +597,14 @@ void GLRenderManager::Run(int frame) {
|
||||
}
|
||||
|
||||
if (IsVRBuild()) {
|
||||
if (PreVRRender()) {
|
||||
int passes = 1;
|
||||
if (!IsMultiviewSupported() && g_Config.bEnableStereo) {
|
||||
passes = 2;
|
||||
}
|
||||
for (int i = 0; i < passes; i++) {
|
||||
PreVRFrameRender(i);
|
||||
queueRunner_.RunSteps(stepsOnThread, skipGLCalls_, i < passes - 1);
|
||||
PostVRFrameRender();
|
||||
}
|
||||
PostVRRender();
|
||||
int passes = 1;
|
||||
if (!IsMultiviewSupported() && g_Config.bEnableStereo) {
|
||||
passes = 2;
|
||||
}
|
||||
for (int i = 0; i < passes; i++) {
|
||||
PreVRFrameRender(i);
|
||||
queueRunner_.RunSteps(stepsOnThread, skipGLCalls_, i < passes - 1);
|
||||
PostVRFrameRender();
|
||||
}
|
||||
} else {
|
||||
queueRunner_.RunSteps(stepsOnThread, skipGLCalls_);
|
||||
|
||||
@@ -1041,6 +1041,7 @@ private:
|
||||
|
||||
bool nextFrame = false;
|
||||
bool firstFrame = true;
|
||||
bool vrRenderStarted = false;
|
||||
|
||||
GLDeleter deleter_;
|
||||
bool skipGLCalls_ = false;
|
||||
|
||||
@@ -401,6 +401,8 @@ public:
|
||||
}
|
||||
|
||||
void BindTextures(int start, int count, Texture **textures) override;
|
||||
void BindNativeTexture(int sampler, void *nativeTexture) override;
|
||||
|
||||
void BindPipeline(Pipeline *pipeline) override;
|
||||
void BindVertexBuffers(int start, int count, Buffer **buffers, const int *offsets) override {
|
||||
_assert_(start + count <= ARRAY_SIZE(curVBuffers_));
|
||||
@@ -1138,6 +1140,12 @@ void OpenGLContext::BindTextures(int start, int count, Texture **textures) {
|
||||
}
|
||||
}
|
||||
|
||||
void OpenGLContext::BindNativeTexture(int index, void *nativeTexture) {
|
||||
GLRTexture *tex = (GLRTexture *)nativeTexture;
|
||||
boundTextures_[index] = tex;
|
||||
renderManager_.BindTexture(index, tex);
|
||||
}
|
||||
|
||||
void OpenGLContext::ApplySamplers() {
|
||||
for (int i = 0; i < MAX_TEXTURE_SLOTS; i++) {
|
||||
const OpenGLSamplerState *samp = boundSamplers_[i];
|
||||
@@ -1483,7 +1491,8 @@ uint32_t OpenGLContext::GetDataFormatSupport(DataFormat fmt) const {
|
||||
return FMT_INPUTLAYOUT;
|
||||
|
||||
case DataFormat::R8_UNORM:
|
||||
return 0;
|
||||
return FMT_TEXTURE;
|
||||
|
||||
case DataFormat::BC1_RGBA_UNORM_BLOCK:
|
||||
case DataFormat::BC2_UNORM_BLOCK:
|
||||
case DataFormat::BC3_UNORM_BLOCK:
|
||||
|
||||
@@ -150,6 +150,8 @@ VKRFramebuffer::VKRFramebuffer(VulkanContext *vk, VkCommandBuffer initCmd, VKRRe
|
||||
width = _width;
|
||||
height = _height;
|
||||
|
||||
_dbg_assert_(tag);
|
||||
|
||||
CreateImage(vulkan_, initCmd, color, width, height, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, true, tag);
|
||||
CreateImage(vulkan_, initCmd, depth, width, height, vulkan_->GetDeviceInfo().preferredDepthStencilFormat, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, false, tag);
|
||||
|
||||
|
||||
@@ -414,6 +414,7 @@ public:
|
||||
|
||||
void BindSamplerStates(int start, int count, SamplerState **state) override;
|
||||
void BindTextures(int start, int count, Texture **textures) override;
|
||||
void BindNativeTexture(int sampler, void *nativeTexture) override;
|
||||
|
||||
void BindPipeline(Pipeline *pipeline) override {
|
||||
curPipeline_ = (VKPipeline *)pipeline;
|
||||
@@ -563,6 +564,11 @@ static int GetBpp(VkFormat format) {
|
||||
case VK_FORMAT_R8G8B8A8_UNORM:
|
||||
case VK_FORMAT_B8G8R8A8_UNORM:
|
||||
return 32;
|
||||
case VK_FORMAT_R8_UNORM:
|
||||
return 8;
|
||||
case VK_FORMAT_R8G8_UNORM:
|
||||
case VK_FORMAT_R16_UNORM:
|
||||
return 16;
|
||||
case VK_FORMAT_R4G4B4A4_UNORM_PACK16:
|
||||
case VK_FORMAT_B4G4R4A4_UNORM_PACK16:
|
||||
case VK_FORMAT_R5G5B5A1_UNORM_PACK16:
|
||||
@@ -586,6 +592,9 @@ static VkFormat DataFormatToVulkan(DataFormat format) {
|
||||
case DataFormat::D32F: return VK_FORMAT_D32_SFLOAT;
|
||||
case DataFormat::D32F_S8: return VK_FORMAT_D32_SFLOAT_S8_UINT;
|
||||
case DataFormat::S8: return VK_FORMAT_S8_UINT;
|
||||
|
||||
case DataFormat::R16_UNORM: return VK_FORMAT_R16_UNORM;
|
||||
|
||||
case DataFormat::R16_FLOAT: return VK_FORMAT_R16_SFLOAT;
|
||||
case DataFormat::R16G16_FLOAT: return VK_FORMAT_R16G16_SFLOAT;
|
||||
case DataFormat::R16G16B16A16_FLOAT: return VK_FORMAT_R16G16B16A16_SFLOAT;
|
||||
@@ -1284,6 +1293,11 @@ void VKContext::BindTextures(int start, int count, Texture **textures) {
|
||||
}
|
||||
}
|
||||
|
||||
void VKContext::BindNativeTexture(int sampler, void *nativeTexture) {
|
||||
boundTextures_[sampler] = nullptr;
|
||||
boundImageView_[sampler] = (VkImageView)nativeTexture;
|
||||
}
|
||||
|
||||
ShaderModule *VKContext::CreateShaderModule(ShaderStage stage, ShaderLanguage language, const uint8_t *data, size_t size, const char *tag) {
|
||||
VKShaderModule *shader = new VKShaderModule(stage, tag);
|
||||
if (shader->Compile(vulkan_, language, data, size)) {
|
||||
|
||||
@@ -33,6 +33,9 @@ size_t DataFormatSizeInBytes(DataFormat fmt) {
|
||||
case DataFormat::R8G8B8A8_SNORM: return 4;
|
||||
case DataFormat::R8G8B8A8_UINT: return 4;
|
||||
case DataFormat::R8G8B8A8_SINT: return 4;
|
||||
|
||||
case DataFormat::R16_UNORM: return 2;
|
||||
|
||||
case DataFormat::R16_FLOAT: return 2;
|
||||
case DataFormat::R16G16_FLOAT: return 4;
|
||||
case DataFormat::R16G16B16A16_FLOAT: return 8;
|
||||
|
||||
@@ -557,6 +557,7 @@ typedef std::function<bool(uint8_t *data, const uint8_t *initData, uint32_t w, u
|
||||
struct TextureDesc {
|
||||
TextureType type;
|
||||
DataFormat format;
|
||||
|
||||
int width;
|
||||
int height;
|
||||
int depth;
|
||||
@@ -674,6 +675,13 @@ public:
|
||||
virtual void BindVertexBuffers(int start, int count, Buffer **buffers, const int *offsets) = 0;
|
||||
virtual void BindIndexBuffer(Buffer *indexBuffer, int offset) = 0;
|
||||
|
||||
// Sometimes it's necessary to bind a texture not created by thin3d, and use with a thin3d pipeline.
|
||||
// Not pretty, and one way in the future could be to create all textures through thin3d.
|
||||
// Data types:
|
||||
// * Vulkan: VkImageView
|
||||
// * D3D11: ID3D11ShaderResourceView*
|
||||
virtual void BindNativeTexture(int sampler, void *nativeTexture) = 0;
|
||||
|
||||
// Only supports a single dynamic uniform buffer, for maximum compatibility with the old APIs and ease of emulation.
|
||||
// More modern methods will be added later.
|
||||
virtual void UpdateDynamicUniformBuffer(const void *ub, size_t size) = 0;
|
||||
|
||||
@@ -628,7 +628,7 @@ void RiscVEmitter::FlushIcache() {
|
||||
|
||||
void RiscVEmitter::FlushIcacheSection(const u8 *start, const u8 *end) {
|
||||
#if PPSSPP_ARCH(RISCV64)
|
||||
__builtin___clear_cache(start, end);
|
||||
__builtin___clear_cache((void *)start, (void *)end);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -284,7 +284,7 @@ void BindVRFramebuffer() {
|
||||
VR_BindFramebuffer(VR_GetEngine());
|
||||
}
|
||||
|
||||
bool PreVRRender() {
|
||||
bool StartVRRender() {
|
||||
if (!VR_GetConfig(VR_CONFIG_VIEWPORT_VALID)) {
|
||||
VR_InitRenderer(VR_GetEngine(), IsMultiviewSupported());
|
||||
VR_SetConfig(VR_CONFIG_VIEWPORT_VALID, true);
|
||||
@@ -312,7 +312,7 @@ bool PreVRRender() {
|
||||
return false;
|
||||
}
|
||||
|
||||
void PostVRRender() {
|
||||
void FinishVRRender() {
|
||||
VR_FinishFrame(VR_GetEngine());
|
||||
}
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ void SetVRCompat(VRCompatFlag flag, long value);
|
||||
|
||||
// VR rendering integration
|
||||
void BindVRFramebuffer();
|
||||
bool PreVRRender();
|
||||
void PostVRRender();
|
||||
bool StartVRRender();
|
||||
void FinishVRRender();
|
||||
void PreVRFrameRender(int fboIndex);
|
||||
void PostVRFrameRender();
|
||||
int GetVRFBOIndex();
|
||||
@@ -64,8 +64,8 @@ inline void SetVRCompat(VRCompatFlag flag, long value) {}
|
||||
|
||||
// VR rendering integration
|
||||
inline void BindVRFramebuffer() {}
|
||||
inline bool PreVRRender() { return false; }
|
||||
inline void PostVRRender() {}
|
||||
inline bool StartVRRender() { return false; }
|
||||
inline void FinishVRRender() {}
|
||||
inline void PreVRFrameRender(int fboIndex) {}
|
||||
inline void PostVRFrameRender() {}
|
||||
inline int GetVRFBOIndex() { return 0; }
|
||||
|
||||
@@ -109,6 +109,7 @@ void Compatibility::CheckSettings(IniFile &iniFile, const std::string &gameID) {
|
||||
CheckSetting(iniFile, gameID, "SplitFramebufferMargin", &flags_.SplitFramebufferMargin);
|
||||
CheckSetting(iniFile, gameID, "ForceLowerResolutionForEffectsOn", &flags_.ForceLowerResolutionForEffectsOn);
|
||||
CheckSetting(iniFile, gameID, "AllowDownloadCLUT", &flags_.AllowDownloadCLUT);
|
||||
CheckSetting(iniFile, gameID, "UploadDepthForCLUTTextures", &flags_.UploadDepthForCLUTTextures);
|
||||
}
|
||||
|
||||
void Compatibility::CheckSetting(IniFile &iniFile, const std::string &gameID, const char *option, bool *flag) {
|
||||
|
||||
@@ -89,6 +89,7 @@ struct CompatFlags {
|
||||
bool SplitFramebufferMargin;
|
||||
bool ForceLowerResolutionForEffectsOn;
|
||||
bool AllowDownloadCLUT;
|
||||
bool UploadDepthForCLUTTextures;
|
||||
};
|
||||
|
||||
struct VRCompat {
|
||||
|
||||
@@ -84,6 +84,9 @@ void GenerateDepalShader300(ShaderWriter &writer, const DepalConfig &config) {
|
||||
|
||||
int shiftedMask = mask << shift;
|
||||
switch (config.bufferFormat) {
|
||||
case GE_FORMAT_CLUT8:
|
||||
writer.C(" int index = int(color.r * 255.99);\n");
|
||||
break;
|
||||
case GE_FORMAT_8888:
|
||||
if (shiftedMask & 0xFF) writer.C(" int r = int(color.r * 255.99);\n"); else writer.C(" int r = 0;\n");
|
||||
if (shiftedMask & 0xFF00) writer.C(" int g = int(color.g * 255.99);\n"); else writer.C(" int g = 0;\n");
|
||||
@@ -168,6 +171,16 @@ void GenerateDepalShaderFloat(ShaderWriter &writer, const DepalConfig &config) {
|
||||
// pixelformat is the format of the texture we are sampling.
|
||||
bool formatOK = true;
|
||||
switch (config.bufferFormat) {
|
||||
case GE_FORMAT_CLUT8:
|
||||
if (shift == 0 && mask == 0xFF) {
|
||||
// Easy peasy.
|
||||
sprintf(lookupMethod, "index.r");
|
||||
formatOK = true;
|
||||
} else {
|
||||
// Deal with this if we find it.
|
||||
formatOK = false;
|
||||
}
|
||||
break;
|
||||
case GE_FORMAT_8888:
|
||||
if ((mask & (mask + 1)) == 0) {
|
||||
// If the value has all bits contiguous (bitmask check above), we can mod by it + 1.
|
||||
|
||||
@@ -734,8 +734,27 @@ bool GenerateFragmentShader(const FShaderID &id, char *buffer, const ShaderLangu
|
||||
WRITE(p, " }\n");
|
||||
break;
|
||||
case ShaderDepalMode::CLUT8_8888:
|
||||
// Not yet implemented.
|
||||
WRITE(p, " vec4 t = vec4(0.0, 0.0, 0.0, 0.0);\n");
|
||||
if (doTextureProjection) {
|
||||
// We don't use textureProj because we need better control and it's probably not much of a savings anyway.
|
||||
// However it is good for precision on older hardware like PowerVR.
|
||||
p.F(" vec2 uv = %s.xy/%s.z;\n vec2 uv_round;\n", texcoord, texcoord);
|
||||
} else {
|
||||
p.F(" vec2 uv = %s.xy;\n vec2 uv_round;\n", texcoord);
|
||||
}
|
||||
p.C(" vec2 tsize = vec2(textureSize(tex, 0).xy);\n");
|
||||
p.C(" uv_round = floor(uv * tsize);\n");
|
||||
p.C(" int component = int(uv_round.x) & 3;\n");
|
||||
p.C(" uv_round.x *= 0.25;\n");
|
||||
p.C(" uv_round /= tsize;\n");
|
||||
p.C(" vec4 t = ").SampleTexture2D("tex", "uv_round").C(";\n");
|
||||
p.C(" int index;\n");
|
||||
p.C(" switch (component) {\n");
|
||||
p.C(" case 0: index = int(t.x * 254.99); break;\n"); // TODO: Not sure why 254.99 instead of 255.99, but it's currently needed.
|
||||
p.C(" case 1: index = int(t.y * 254.99); break;\n");
|
||||
p.C(" case 2: index = int(t.z * 254.99); break;\n");
|
||||
p.C(" case 3: index = int(t.w * 254.99); break;\n");
|
||||
p.C(" }\n");
|
||||
p.C(" t = ").LoadTexture2D("pal", "ivec2(index, 0)", 0).C(";\n");
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -475,9 +475,9 @@ VirtualFramebuffer *FramebufferManagerCommon::DoSetRenderFrameBuffer(Framebuffer
|
||||
vfb->fb_format = params.fb_format;
|
||||
vfb->usageFlags = FB_USAGE_RENDER_COLOR;
|
||||
|
||||
u32 byteSize = ColorBufferByteSize(vfb);
|
||||
if (Memory::IsVRAMAddress(params.fb_address) && params.fb_address + byteSize > framebufRangeEnd_) {
|
||||
framebufRangeEnd_ = params.fb_address + byteSize;
|
||||
u32 colorByteSize = ColorBufferByteSize(vfb);
|
||||
if (Memory::IsVRAMAddress(params.fb_address) && params.fb_address + colorByteSize > framebufRangeEnd_) {
|
||||
framebufRangeEnd_ = params.fb_address + colorByteSize;
|
||||
}
|
||||
|
||||
// This is where we actually create the framebuffer. The true is "force".
|
||||
@@ -499,9 +499,9 @@ VirtualFramebuffer *FramebufferManagerCommon::DoSetRenderFrameBuffer(Framebuffer
|
||||
|
||||
// Assume that if we're clearing right when switching to a new framebuffer, we don't need to upload.
|
||||
if (useBufferedRendering_ && params.isDrawing) {
|
||||
gpu->PerformMemoryUpload(params.fb_address, byteSize);
|
||||
gpu->PerformMemoryUpload(params.fb_address, colorByteSize);
|
||||
// Alpha was already done by PerformMemoryUpload.
|
||||
PerformStencilUpload(params.fb_address, byteSize, StencilUpload::STENCIL_IS_ZERO | StencilUpload::IGNORE_ALPHA);
|
||||
PerformStencilUpload(params.fb_address, colorByteSize, StencilUpload::STENCIL_IS_ZERO | StencilUpload::IGNORE_ALPHA);
|
||||
// TODO: Is it worth trying to upload the depth buffer (only if it wasn't copied above..?)
|
||||
}
|
||||
|
||||
@@ -551,9 +551,23 @@ void FramebufferManagerCommon::SetDepthFrameBuffer(bool isClearingDepth) {
|
||||
// by copying from any overlapping buffers with fresher content.
|
||||
if (!isClearingDepth) {
|
||||
CopyToDepthFromOverlappingFramebuffers(currentRenderVfb_);
|
||||
|
||||
// Special compatibility trick for Burnout Dominator lens flares. Not sure how to best generalize this. See issue #11100
|
||||
if (PSP_CoreParameter().compat.flags().UploadDepthForCLUTTextures && (currentRenderVfb_->usageFlags & FB_USAGE_CLUT) != 0) {
|
||||
// Set the flag, then upload memory contents to depth channel.
|
||||
// Sanity check the depth buffer pointer.
|
||||
if (currentRenderVfb_->z_address != 0 && currentRenderVfb_->z_address != currentRenderVfb_->fb_address) {
|
||||
if (Memory::IsValidRange(currentRenderVfb_->z_address, currentRenderVfb_->width * 2)) {
|
||||
const u16 *src = (const u16 *)Memory::GetPointerUnchecked(currentRenderVfb_->z_address);
|
||||
DrawPixels(currentRenderVfb_, 0, 0, (const u8 *)src, GE_FORMAT_DEPTH16, currentRenderVfb_->z_stride, currentRenderVfb_->width, currentRenderVfb_->height, RASTER_DEPTH, "Depth Upload");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// First time use of this framebuffer's depth buffer.
|
||||
currentRenderVfb_->usageFlags |= FB_USAGE_RENDER_DEPTH;
|
||||
|
||||
currentRenderVfb_->depthBindSeq = GetBindSeqCount();
|
||||
}
|
||||
|
||||
@@ -784,8 +798,8 @@ void FramebufferManagerCommon::CopyToColorFromOverlappingFramebuffers(VirtualFra
|
||||
|
||||
float scaleFactorX = 1.0f;
|
||||
pipeline = GetReinterpretPipeline(src->fb_format, dst->fb_format, &scaleFactorX);
|
||||
dstX1 *= 0.5f;
|
||||
dstX2 *= 0.5f;
|
||||
dstX1 *= scaleFactorX;
|
||||
dstX2 *= scaleFactorX;
|
||||
|
||||
pass_name = reinterpretStrings[(int)src->fb_format][(int)dst->fb_format];
|
||||
|
||||
@@ -811,6 +825,11 @@ void FramebufferManagerCommon::CopyToColorFromOverlappingFramebuffers(VirtualFra
|
||||
}
|
||||
|
||||
Draw2DPipeline *FramebufferManagerCommon::GetReinterpretPipeline(GEBufferFormat from, GEBufferFormat to, float *scaleFactorX) {
|
||||
if (from == to) {
|
||||
*scaleFactorX = 1.0f;
|
||||
return Get2DPipeline(DRAW2D_COPY_COLOR);
|
||||
}
|
||||
|
||||
if (IsBufferFormat16Bit(from) && !IsBufferFormat16Bit(to)) {
|
||||
// We halve the X coordinates in the destination framebuffer.
|
||||
// The shader will collect two pixels worth of input data and merge into one.
|
||||
@@ -819,6 +838,8 @@ Draw2DPipeline *FramebufferManagerCommon::GetReinterpretPipeline(GEBufferFormat
|
||||
// We double the X coordinates in the destination framebuffer.
|
||||
// The shader will sample and depending on the X coordinate & 1, use the upper or lower bits.
|
||||
*scaleFactorX = 2.0f;
|
||||
} else {
|
||||
*scaleFactorX = 1.0f;
|
||||
}
|
||||
|
||||
Draw2DPipeline *pipeline = reinterpretFromTo_[(int)from][(int)to];
|
||||
@@ -1017,7 +1038,7 @@ void FramebufferManagerCommon::UpdateFromMemory(u32 addr, int size) {
|
||||
// TODO: This doesn't seem quite right anymore.
|
||||
fmt = displayFormat_;
|
||||
}
|
||||
DrawPixels(vfb, 0, 0, Memory::GetPointer(addr), fmt, vfb->fb_stride, vfb->width, vfb->height);
|
||||
DrawPixels(vfb, 0, 0, Memory::GetPointer(addr), fmt, vfb->fb_stride, vfb->width, vfb->height, RASTER_COLOR, "UpdateFromMemory_DrawPixels");
|
||||
SetColorUpdated(vfb, gstate_c.skipDrawReason);
|
||||
} else {
|
||||
INFO_LOG(FRAMEBUF, "Invalidating FBO for %08x (%dx%d %s)", vfb->fb_address, vfb->width, vfb->height, GeBufferFormatToString(vfb->fb_format));
|
||||
@@ -1033,20 +1054,20 @@ void FramebufferManagerCommon::UpdateFromMemory(u32 addr, int size) {
|
||||
gstate_c.Dirty(DIRTY_FRAGMENTSHADER_STATE);
|
||||
}
|
||||
|
||||
void FramebufferManagerCommon::DrawPixels(VirtualFramebuffer *vfb, int dstX, int dstY, const u8 *srcPixels, GEBufferFormat srcPixelFormat, int srcStride, int width, int height) {
|
||||
void FramebufferManagerCommon::DrawPixels(VirtualFramebuffer *vfb, int dstX, int dstY, const u8 *srcPixels, GEBufferFormat srcPixelFormat, int srcStride, int width, int height, RasterChannel channel, const char *tag) {
|
||||
textureCache_->ForgetLastTexture();
|
||||
shaderManager_->DirtyLastShader(); // On GL, important that this is BEFORE drawing
|
||||
shaderManager_->DirtyLastShader();
|
||||
float u0 = 0.0f, u1 = 1.0f;
|
||||
float v0 = 0.0f, v1 = 1.0f;
|
||||
|
||||
DrawTextureFlags flags;
|
||||
if (useBufferedRendering_ && vfb && vfb->fbo) {
|
||||
flags = DRAWTEX_LINEAR;
|
||||
draw_->BindFramebufferAsRenderTarget(vfb->fbo, { Draw::RPAction::KEEP, Draw::RPAction::KEEP, Draw::RPAction::KEEP }, "DrawPixels");
|
||||
gstate_c.Dirty(DIRTY_VIEWPORTSCISSOR_STATE);
|
||||
flags = channel == RASTER_COLOR ? DRAWTEX_LINEAR : DRAWTEX_NEAREST;
|
||||
draw_->BindFramebufferAsRenderTarget(vfb->fbo, { Draw::RPAction::KEEP, Draw::RPAction::KEEP, Draw::RPAction::KEEP }, tag);
|
||||
SetViewport2D(0, 0, vfb->renderWidth, vfb->renderHeight);
|
||||
draw_->SetScissorRect(0, 0, vfb->renderWidth, vfb->renderHeight);
|
||||
} else {
|
||||
_dbg_assert_(channel == RASTER_COLOR);
|
||||
// We are drawing directly to the back buffer so need to flip.
|
||||
// Should more of this be handled by the presentation engine?
|
||||
if (needBackBufferYSwap_)
|
||||
@@ -1060,10 +1081,18 @@ void FramebufferManagerCommon::DrawPixels(VirtualFramebuffer *vfb, int dstX, int
|
||||
draw_->SetScissorRect(0, 0, pixelWidth_, pixelHeight_);
|
||||
}
|
||||
|
||||
if (channel == RASTER_DEPTH) {
|
||||
_dbg_assert_(srcPixelFormat == GE_FORMAT_DEPTH16);
|
||||
flags = flags | DRAWTEX_DEPTH;
|
||||
}
|
||||
|
||||
Draw::Texture *pixelsTex = MakePixelTexture(srcPixels, srcPixelFormat, srcStride, width, height);
|
||||
if (pixelsTex) {
|
||||
draw_->BindTextures(0, 1, &pixelsTex);
|
||||
|
||||
// TODO: Replace with draw2D_.Blit() directly.
|
||||
DrawActiveTexture(dstX, dstY, width, height, vfb->bufferWidth, vfb->bufferHeight, u0, v0, u1, v1, ROTATION_LOCKED_HORIZONTAL, flags);
|
||||
|
||||
gpuStats.numUploads++;
|
||||
pixelsTex->Release();
|
||||
draw_->InvalidateCachedState();
|
||||
@@ -1081,7 +1110,7 @@ bool FramebufferManagerCommon::BindFramebufferAsColorTexture(int stage, VirtualF
|
||||
|
||||
// currentRenderVfb_ will always be set when this is called, except from the GE debugger.
|
||||
// Let's just not bother with the copy in that case.
|
||||
bool skipCopy = !(flags & BINDFBCOLOR_MAY_COPY) || GPUStepping::IsStepping();
|
||||
bool skipCopy = !(flags & BINDFBCOLOR_MAY_COPY);
|
||||
|
||||
// Currently rendering to this framebuffer. Need to make a copy.
|
||||
if (!skipCopy && framebuffer == currentRenderVfb_) {
|
||||
@@ -1153,6 +1182,7 @@ Draw::Texture *FramebufferManagerCommon::MakePixelTexture(const u8 *srcPixels, G
|
||||
const u16_le *src16 = (const u16_le *)srcPixels + srcStride * y;
|
||||
const u32_le *src32 = (const u32_le *)srcPixels + srcStride * y;
|
||||
u32 *dst = (u32 *)(data + byteStride * y);
|
||||
u16 *dst16 = (u16 *)(data + byteStride * y);
|
||||
switch (srcPixelFormat) {
|
||||
case GE_FORMAT_565:
|
||||
if (preferredPixelsFormat_ == Draw::DataFormat::B8G8R8A8_UNORM)
|
||||
@@ -1185,18 +1215,28 @@ Draw::Texture *FramebufferManagerCommon::MakePixelTexture(const u8 *srcPixels, G
|
||||
memcpy(dst, src32, width * 4);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_INVALID:
|
||||
case GE_FORMAT_DEPTH16:
|
||||
_dbg_assert_msg_(false, "Invalid pixelFormat passed to DrawPixels().");
|
||||
// TODO: Must take the depth range into account, unless it's already 0-1.
|
||||
// TODO: Depending on the color buffer format used with this depth buffer, we need
|
||||
// to do one of two different swizzle operations. However, for the only use of this so far,
|
||||
// the Burnout lens flare trickery, swizzle doesn't matter since it's just a 0, 7fff, 0, 7fff pattern
|
||||
// which comes out the same.
|
||||
memcpy(dst16, src16, w * 2);
|
||||
break;
|
||||
|
||||
case GE_FORMAT_INVALID:
|
||||
// Bad
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Note: For depth, we create an R16_UNORM texture, that'll be just fine for uploading depth through a shader,
|
||||
// and likely more efficient.
|
||||
Draw::TextureDesc desc{
|
||||
Draw::TextureType::LINEAR2D,
|
||||
preferredPixelsFormat_,
|
||||
srcPixelFormat == GE_FORMAT_DEPTH16 ? Draw::DataFormat::R16_UNORM : preferredPixelsFormat_,
|
||||
width,
|
||||
height,
|
||||
1,
|
||||
@@ -1206,11 +1246,12 @@ Draw::Texture *FramebufferManagerCommon::MakePixelTexture(const u8 *srcPixels, G
|
||||
{ (uint8_t *)srcPixels },
|
||||
generateTexture,
|
||||
};
|
||||
|
||||
// Hot Shots Golf (#12355) does tons of these in a frame in some situations! So creating textures
|
||||
// better be fast.
|
||||
Draw::Texture *tex = draw_->CreateTexture(desc);
|
||||
if (!tex)
|
||||
ERROR_LOG(G3D, "Failed to create drawpixels texture");
|
||||
ERROR_LOG(G3D, "Failed to create DrawPixels texture");
|
||||
return tex;
|
||||
}
|
||||
|
||||
@@ -1229,7 +1270,7 @@ void FramebufferManagerCommon::DrawFramebufferToOutput(const u8 *srcPixels, int
|
||||
if (needBackBufferYSwap_) {
|
||||
flags |= OutputFlags::BACKBUFFER_FLIPPED;
|
||||
}
|
||||
// DrawActiveTexture reverses these, probably to match "up".
|
||||
// CopyToOutput reverses these, probably to match "up".
|
||||
if (GetGPUBackend() == GPUBackend::DIRECT3D9 || GetGPUBackend() == GPUBackend::DIRECT3D11) {
|
||||
flags |= OutputFlags::POSITION_FLIPPED;
|
||||
}
|
||||
@@ -1654,7 +1695,7 @@ bool FramebufferManagerCommon::NotifyFramebufferCopy(u32 src, u32 dst, int size,
|
||||
WARN_LOG_ONCE(btucpy, G3D, "Memcpy fbo upload %08x -> %08x (size: %x)", src, dst, size);
|
||||
FlushBeforeCopy();
|
||||
const u8 *srcBase = Memory::GetPointerUnchecked(src);
|
||||
DrawPixels(dstBuffer, 0, dstY, srcBase, dstBuffer->fb_format, dstBuffer->fb_stride, dstBuffer->width, dstH);
|
||||
DrawPixels(dstBuffer, 0, dstY, srcBase, dstBuffer->fb_format, dstBuffer->fb_stride, dstBuffer->width, dstH, RASTER_COLOR, "MemcpyFboUpload_DrawPixels");
|
||||
SetColorUpdated(dstBuffer, skipDrawReason);
|
||||
RebindFramebuffer("RebindFramebuffer - Memcpy fbo upload");
|
||||
// This is a memcpy, let's still copy just in case.
|
||||
@@ -1700,7 +1741,16 @@ bool FramebufferManagerCommon::FindTransferFramebuffer(u32 basePtr, int stride_p
|
||||
// that of their buffers. Then after matching we try to map the copy to the simplest operation that does
|
||||
// what we need.
|
||||
|
||||
// We are only looking at color for now, have not found any block transfers of depth data (although it's plausible).
|
||||
|
||||
for (auto vfb : vfbs_) {
|
||||
// Check for easily detected depth copies for logging purposes.
|
||||
// Depth copies are not that useful though because you manually need to account for swizzle, so
|
||||
// not sure if games will use them.
|
||||
if ((vfb->z_address & 0x3FFFFFFF) == basePtr) {
|
||||
WARN_LOG_N_TIMES(z_xfer, 5, G3D, "FindTransferFramebuffer: found matching depth buffer, %08x (dest=%d, bpp=%d)", basePtr, (int)destination, bpp);
|
||||
}
|
||||
|
||||
const u32 vfb_address = vfb->fb_address & 0x3FFFFFFF;
|
||||
const u32 vfb_size = ColorBufferByteSize(vfb);
|
||||
|
||||
@@ -1773,7 +1823,14 @@ bool FramebufferManagerCommon::FindTransferFramebuffer(u32 basePtr, int stride_p
|
||||
// Sort candidates by just recency for now, we might add other.
|
||||
for (size_t i = 0; i < candidates.size(); i++) {
|
||||
const BlockTransferRect *candidate = &candidates[i];
|
||||
if (!best || candidate->vfb->colorBindSeq > best->vfb->colorBindSeq) {
|
||||
|
||||
bool better = !best || candidate->vfb->colorBindSeq > best->vfb->colorBindSeq;
|
||||
if ((candidate->vfb->usageFlags & FB_USAGE_CLUT) && candidate->x_bytes == 0 && candidate->y == 0 && destination) {
|
||||
// Hack to prioritize copies to clut buffers.
|
||||
best = candidate;
|
||||
break;
|
||||
}
|
||||
if (better) {
|
||||
best = candidate;
|
||||
}
|
||||
}
|
||||
@@ -2150,9 +2207,10 @@ void FramebufferManagerCommon::NotifyBlockTransferAfter(u32 dstBasePtr, int dstS
|
||||
}
|
||||
|
||||
if (dstBuffer && !srcBuffer) {
|
||||
WARN_LOG_ONCE(btu, G3D, "Block transfer upload %08x -> %08x", srcBasePtr, dstBasePtr);
|
||||
WARN_LOG_ONCE(btu, G3D, "Block transfer upload %08x -> %08x (%dx%d %d,%d bpp=%d)", srcBasePtr, dstBasePtr, width, height, dstX, dstY, bpp);
|
||||
FlushBeforeCopy();
|
||||
const u8 *srcBase = Memory::GetPointerUnchecked(srcBasePtr) + (srcX + srcY * srcStride) * bpp;
|
||||
|
||||
int dstBpp = BufferFormatBytesPerPixel(dstRect.vfb->fb_format);
|
||||
float dstXFactor = (float)bpp / dstBpp;
|
||||
if (dstRect.w_bytes / bpp > dstRect.vfb->width || dstRect.h > dstRect.vfb->height) {
|
||||
@@ -2166,7 +2224,7 @@ void FramebufferManagerCommon::NotifyBlockTransferAfter(u32 dstBasePtr, int dstS
|
||||
// Resizing may change the viewport/etc.
|
||||
gstate_c.Dirty(DIRTY_VIEWPORTSCISSOR_STATE | DIRTY_CULLRANGE);
|
||||
}
|
||||
DrawPixels(dstRect.vfb, static_cast<int>(dstX * dstXFactor), dstY, srcBase, dstRect.vfb->fb_format, static_cast<int>(srcStride * dstXFactor), static_cast<int>(dstRect.w_bytes / bpp * dstXFactor), dstRect.h);
|
||||
DrawPixels(dstRect.vfb, static_cast<int>(dstX * dstXFactor), dstY, srcBase, dstRect.vfb->fb_format, static_cast<int>(srcStride * dstXFactor), static_cast<int>(dstRect.w_bytes / bpp * dstXFactor), dstRect.h, RASTER_COLOR, "BlockTransferCopy_DrawPixels");
|
||||
SetColorUpdated(dstRect.vfb, skipDrawReason);
|
||||
RebindFramebuffer("RebindFramebuffer - NotifyBlockTransferAfter");
|
||||
}
|
||||
@@ -2706,7 +2764,7 @@ void FramebufferManagerCommon::DrawActiveTexture(float x, float y, float w, floa
|
||||
// Rearrange to strip form.
|
||||
std::swap(coord[2], coord[3]);
|
||||
|
||||
draw2D_.DrawStrip2D(nullptr, coord, 4, (flags & DRAWTEX_LINEAR) != 0, Get2DPipeline(DRAW2D_COPY_COLOR));
|
||||
draw2D_.DrawStrip2D(nullptr, coord, 4, (flags & DRAWTEX_LINEAR) != 0, Get2DPipeline((flags & DRAWTEX_DEPTH) ? DRAW2D_COPY_DEPTH : DRAW2D_COPY_COLOR));
|
||||
|
||||
gstate_c.Dirty(DIRTY_BLEND_STATE | DIRTY_RASTER_STATE | DIRTY_DEPTHSTENCIL_STATE | DIRTY_VIEWPORTSCISSOR_STATE | DIRTY_TEXTURE_IMAGE | DIRTY_TEXTURE_PARAMS | DIRTY_VERTEXSHADER_STATE | DIRTY_FRAGMENTSHADER_STATE);
|
||||
}
|
||||
@@ -2926,6 +2984,10 @@ static void ApplyKillzoneFramebufferSplit(FramebufferHeuristicParams *params, in
|
||||
margin = true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Implement this for Burnout Dominator. It has to handle self-reads inside
|
||||
// the margin framebuffer though, so framebuffer copies are still needed, just smaller.
|
||||
// It uses 0x0080019f (through, float texcoords, ABGR 8888 colors, float positions).
|
||||
}
|
||||
|
||||
if (margin) {
|
||||
|
||||
@@ -192,6 +192,7 @@ enum DrawTextureFlags {
|
||||
DRAWTEX_NEAREST = 0,
|
||||
DRAWTEX_LINEAR = 1,
|
||||
DRAWTEX_TO_BACKBUFFER = 8,
|
||||
DRAWTEX_DEPTH = 16,
|
||||
};
|
||||
|
||||
inline DrawTextureFlags operator | (const DrawTextureFlags &lhs, const DrawTextureFlags &rhs) {
|
||||
@@ -327,7 +328,7 @@ public:
|
||||
void DownloadFramebufferForClut(u32 fb_address, u32 loadBytes);
|
||||
void DrawFramebufferToOutput(const u8 *srcPixels, int srcStride, GEBufferFormat srcPixelFormat);
|
||||
|
||||
void DrawPixels(VirtualFramebuffer *vfb, int dstX, int dstY, const u8 *srcPixels, GEBufferFormat srcPixelFormat, int srcStride, int width, int height);
|
||||
void DrawPixels(VirtualFramebuffer *vfb, int dstX, int dstY, const u8 *srcPixels, GEBufferFormat srcPixelFormat, int srcStride, int width, int height, RasterChannel channel, const char *tag);
|
||||
|
||||
size_t NumVFBs() const { return vfbs_.size(); }
|
||||
|
||||
@@ -422,8 +423,18 @@ public:
|
||||
VirtualFramebuffer *ResolveFramebufferColorToFormat(VirtualFramebuffer *vfb, GEBufferFormat newFormat);
|
||||
|
||||
Draw2DPipeline *Get2DPipeline(Draw2DShader shader);
|
||||
|
||||
// If from==to, returns a copy pipeline.
|
||||
Draw2DPipeline *GetReinterpretPipeline(GEBufferFormat from, GEBufferFormat to, float *scaleFactorX);
|
||||
|
||||
// Public to be used from the texture cache's depal shenanigans.
|
||||
void BlitUsingRaster(
|
||||
Draw::Framebuffer *src, float srcX1, float srcY1, float srcX2, float srcY2,
|
||||
Draw::Framebuffer *dest, float destX1, float destY1, float destX2, float destY2,
|
||||
bool linearFilter,
|
||||
int scaleFactor, // usually unused, except for swizzle...
|
||||
Draw2DPipeline *pipeline, const char *tag);
|
||||
|
||||
protected:
|
||||
virtual void PackFramebufferSync(VirtualFramebuffer *vfb, int x, int y, int w, int h, RasterChannel channel);
|
||||
void SetViewport2D(int x, int y, int w, int h);
|
||||
@@ -441,13 +452,6 @@ protected:
|
||||
// Used by ReadFramebufferToMemory and later framebuffer block copies
|
||||
void BlitFramebuffer(VirtualFramebuffer *dst, int dstX, int dstY, VirtualFramebuffer *src, int srcX, int srcY, int w, int h, int bpp, RasterChannel channel, const char *tag);
|
||||
|
||||
void BlitUsingRaster(
|
||||
Draw::Framebuffer *src, float srcX1, float srcY1, float srcX2, float srcY2,
|
||||
Draw::Framebuffer *dest, float destX1, float destY1, float destX2, float destY2,
|
||||
bool linearFilter,
|
||||
int scaleFactor, // usually unused, except for swizzle...
|
||||
Draw2DPipeline *pipeline, const char *tag);
|
||||
|
||||
void CopyFramebufferForColorTexture(VirtualFramebuffer *dst, VirtualFramebuffer *src, int flags);
|
||||
|
||||
void EstimateDrawingSize(u32 fb_address, int fb_stride, GEBufferFormat fb_format, int viewport_width, int viewport_height, int region_width, int region_height, int scissor_width, int scissor_height, int &drawing_width, int &drawing_height);
|
||||
|
||||
@@ -856,7 +856,7 @@ static inline bool blendColorSimilar(uint32_t a, uint32_t b, int margin = 25) {
|
||||
// Try to simulate some common logic ops by using blend, if needed.
|
||||
// The shader might also need modification, the below function SimulateLogicOpShaderTypeIfNeeded
|
||||
// takes care of that.
|
||||
static void SimulateLogicOpIfNeeded(BlendFactor &srcBlend, BlendFactor &dstBlend, BlendEq &blendEq) {
|
||||
static bool SimulateLogicOpIfNeeded(BlendFactor &srcBlend, BlendFactor &dstBlend, BlendEq &blendEq) {
|
||||
// Note: our shader solution applies logic ops BEFORE blending, not correctly after.
|
||||
// This is however fine for the most common ones, like CLEAR/NOOP/SET, etc.
|
||||
if (!gstate_c.Supports(GPU_SUPPORTS_LOGIC_OP)) {
|
||||
@@ -866,7 +866,7 @@ static void SimulateLogicOpIfNeeded(BlendFactor &srcBlend, BlendFactor &dstBlend
|
||||
srcBlend = BlendFactor::ZERO;
|
||||
dstBlend = BlendFactor::ZERO;
|
||||
blendEq = BlendEq::ADD;
|
||||
break;
|
||||
return true;
|
||||
case GE_LOGIC_AND:
|
||||
case GE_LOGIC_AND_REVERSE:
|
||||
WARN_LOG_REPORT_ONCE(d3dLogicOpAnd, G3D, "Unsupported AND logic op: %x", gstate.getLogicOp());
|
||||
@@ -889,21 +889,23 @@ static void SimulateLogicOpIfNeeded(BlendFactor &srcBlend, BlendFactor &dstBlend
|
||||
dstBlend = BlendFactor::ONE;
|
||||
blendEq = BlendEq::SUBTRACT;
|
||||
WARN_LOG_REPORT_ONCE(d3dLogicOpInverted, G3D, "Attempted inverse for logic op: %x", gstate.getLogicOp());
|
||||
break;
|
||||
return true;
|
||||
case GE_LOGIC_NOOP:
|
||||
srcBlend = BlendFactor::ZERO;
|
||||
dstBlend = BlendFactor::ONE;
|
||||
blendEq = BlendEq::ADD;
|
||||
break;
|
||||
return true;
|
||||
case GE_LOGIC_XOR:
|
||||
WARN_LOG_REPORT_ONCE(d3dLogicOpOrXor, G3D, "Unsupported XOR logic op: %x", gstate.getLogicOp());
|
||||
break;
|
||||
case GE_LOGIC_OR:
|
||||
case GE_LOGIC_OR_INVERTED:
|
||||
// Inverted in shader.
|
||||
srcBlend = BlendFactor::ONE;
|
||||
dstBlend = BlendFactor::ONE;
|
||||
blendEq = BlendEq::ADD;
|
||||
WARN_LOG_REPORT_ONCE(d3dLogicOpOr, G3D, "Attempted or for logic op: %x", gstate.getLogicOp());
|
||||
break;
|
||||
return true;
|
||||
case GE_LOGIC_OR_REVERSE:
|
||||
WARN_LOG_REPORT_ONCE(d3dLogicOpOrReverse, G3D, "Unsupported OR REVERSE logic op: %x", gstate.getLogicOp());
|
||||
break;
|
||||
@@ -912,10 +914,12 @@ static void SimulateLogicOpIfNeeded(BlendFactor &srcBlend, BlendFactor &dstBlend
|
||||
dstBlend = BlendFactor::ONE;
|
||||
blendEq = BlendEq::ADD;
|
||||
WARN_LOG_REPORT_ONCE(d3dLogicOpSet, G3D, "Attempted set for logic op: %x", gstate.getLogicOp());
|
||||
break;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Choose the shader part of the above logic op fallback simulation.
|
||||
@@ -950,7 +954,6 @@ void ApplyStencilReplaceAndLogicOpIgnoreBlend(ReplaceAlphaType replaceAlphaWithS
|
||||
BlendFactor srcBlend = BlendFactor::ONE;
|
||||
BlendFactor dstBlend = BlendFactor::ZERO;
|
||||
BlendEq blendEq = BlendEq::ADD;
|
||||
SimulateLogicOpIfNeeded(srcBlend, dstBlend, blendEq);
|
||||
|
||||
// We're not blending, but we may still want to "blend" for stencil.
|
||||
// This is only useful for INCR/DECR/INVERT. Others can write directly.
|
||||
@@ -1252,11 +1255,6 @@ static void ConvertBlendState(GenericBlendState &blendState, bool forceReplaceBl
|
||||
colorEq = eqLookupNoMinMax[blendFuncEq];
|
||||
}
|
||||
|
||||
// Attempt to apply simulated logic ops, if any and if needed.
|
||||
if (!forceReplaceBlend) {
|
||||
SimulateLogicOpIfNeeded(glBlendFuncA, glBlendFuncB, colorEq);
|
||||
}
|
||||
|
||||
// The stencil-to-alpha in fragment shader doesn't apply here (blending is enabled), and we shouldn't
|
||||
// do any blending in the alpha channel as that doesn't seem to happen on PSP. So, we attempt to
|
||||
// apply the stencil to the alpha, since that's what should be stored.
|
||||
@@ -1581,5 +1579,20 @@ void ComputedPipelineState::Convert(bool shaderBitOpsSuppported) {
|
||||
if (blendState.applyFramebufferRead || logicState.applyFramebufferRead) {
|
||||
maskState.ConvertToShaderBlend();
|
||||
logicState.ConvertToShaderBlend();
|
||||
} else {
|
||||
// If it isn't a read, we may need to change blending to apply the logic op.
|
||||
logicState.ApplyToBlendState(blendState);
|
||||
}
|
||||
}
|
||||
|
||||
void GenericLogicState::ApplyToBlendState(GenericBlendState &blendState) {
|
||||
if (SimulateLogicOpIfNeeded(blendState.srcColor, blendState.dstColor, blendState.eqColor)) {
|
||||
if (!blendState.blendEnabled) {
|
||||
// If it wasn't turned on, make sure it is now.
|
||||
blendState.blendEnabled = true;
|
||||
blendState.srcAlpha = BlendFactor::ONE;
|
||||
blendState.dstAlpha = BlendFactor::ZERO;
|
||||
blendState.eqAlpha = BlendEq::ADD;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -226,6 +226,7 @@ struct GenericLogicState {
|
||||
// Hardware and shader generation
|
||||
GELogicOp logicOp;
|
||||
|
||||
void ApplyToBlendState(GenericBlendState &blendState);
|
||||
void ConvertToShaderBlend() {
|
||||
if (logicOp != GE_LOGIC_COPY) {
|
||||
logicOpEnabled = false;
|
||||
|
||||
@@ -180,7 +180,7 @@ std::string FragmentShaderDesc(const FShaderID &id) {
|
||||
if (id.Bit(FS_BIT_COLOR_DOUBLE)) desc << "2x ";
|
||||
if (id.Bit(FS_BIT_FLATSHADE)) desc << "Flat ";
|
||||
if (id.Bit(FS_BIT_BGRA_TEXTURE)) desc << "BGRA ";
|
||||
switch ((ShaderDepalMode)id.Bit(FS_BIT_SHADER_DEPAL_MODE)) {
|
||||
switch ((ShaderDepalMode)id.Bits(FS_BIT_SHADER_DEPAL_MODE, 2)) {
|
||||
case ShaderDepalMode::OFF: break;
|
||||
case ShaderDepalMode::NORMAL: desc << "Depal "; break;
|
||||
case ShaderDepalMode::SMOOTHED: desc << "SmoothDepal "; break;
|
||||
|
||||
@@ -36,14 +36,14 @@ struct UB_VS_FS_Base {
|
||||
uint32_t spline_counts; uint32_t depal_mask_shift_off_fmt; // 4 params packed into one.
|
||||
uint32_t colorWriteMask; float mipBias;
|
||||
// Fragment data
|
||||
float fogColor[4];
|
||||
float texEnvColor[4];
|
||||
float fogColor[4]; // .w is unused
|
||||
float texEnvColor[4]; // .w is unused
|
||||
int alphaColorRef[4];
|
||||
int colorTestMask[4];
|
||||
float blendFixA[4];
|
||||
float blendFixB[4];
|
||||
float blendFixA[4]; // .w is unused
|
||||
float blendFixB[4]; // .w is unused
|
||||
float texClamp[4];
|
||||
float texClampOffset[4];
|
||||
float texClampOffset[4]; // .zw are unused
|
||||
};
|
||||
|
||||
static const char *ub_baseStr =
|
||||
|
||||
@@ -377,13 +377,22 @@ TexCacheEntry *TextureCacheCommon::SetTexture() {
|
||||
}
|
||||
|
||||
bool hasClut = gstate.isTextureFormatIndexed();
|
||||
bool hasClutGPU = false;
|
||||
u32 cluthash;
|
||||
if (hasClut) {
|
||||
if (clutLastFormat_ != gstate.clutformat) {
|
||||
// We update here because the clut format can be specified after the load.
|
||||
UpdateCurrentClut(gstate.getClutPaletteFormat(), gstate.getClutIndexStartPos(), gstate.isClutIndexSimple());
|
||||
if (clutRenderAddress_ != 0xFFFFFFFF) {
|
||||
gstate_c.curTextureXOffset = 0.0f;
|
||||
gstate_c.curTextureYOffset = 0.0f;
|
||||
hasClutGPU = true;
|
||||
cluthash = 0; // Or should we use some other marker value?
|
||||
} else {
|
||||
if (clutLastFormat_ != gstate.clutformat) {
|
||||
// We update here because the clut format can be specified after the load.
|
||||
// TODO: Unify this as far as possible (I think only GLES backend really needs its own implementation due to different component order).
|
||||
UpdateCurrentClut(gstate.getClutPaletteFormat(), gstate.getClutIndexStartPos(), gstate.isClutIndexSimple());
|
||||
}
|
||||
cluthash = clutHash_ ^ gstate.clutformat;
|
||||
}
|
||||
cluthash = clutHash_ ^ gstate.clutformat;
|
||||
} else {
|
||||
cluthash = 0;
|
||||
}
|
||||
@@ -412,6 +421,13 @@ TexCacheEntry *TextureCacheCommon::SetTexture() {
|
||||
bool match = entry->Matches(dim, texFormat, maxLevel);
|
||||
const char *reason = "different params";
|
||||
|
||||
// Check for dynamic CLUT status
|
||||
if (((entry->status & TexCacheEntry::STATUS_CLUT_GPU) != 0) != hasClutGPU) {
|
||||
// Need to recreate, suddenly a CLUT GPU texture was used without it, or vice versa.
|
||||
// I think this can only happen on a clut hash collision with the marker value, so highly unlikely.
|
||||
match = false;
|
||||
}
|
||||
|
||||
// Check for FBO changes.
|
||||
if (entry->status & TexCacheEntry::STATUS_FRAMEBUFFER_OVERLAP) {
|
||||
// Fall through to the end where we'll delete the entry if there's a framebuffer.
|
||||
@@ -553,10 +569,6 @@ TexCacheEntry *TextureCacheCommon::SetTexture() {
|
||||
entry = new TexCacheEntry{};
|
||||
cache_[cachekey].reset(entry);
|
||||
|
||||
if (hasClut && clutRenderAddress_ != 0xFFFFFFFF) {
|
||||
WARN_LOG_REPORT_ONCE(clutUseRender, G3D, "Using texture with rendered CLUT: texfmt=%d, clutfmt=%d", gstate.getTextureFormat(), gstate.getClutPaletteFormat());
|
||||
}
|
||||
|
||||
if (PPGeIsFontTextureAddress(texaddr)) {
|
||||
// It's the builtin font texture.
|
||||
entry->status = TexCacheEntry::STATUS_RELIABLE;
|
||||
@@ -566,6 +578,11 @@ TexCacheEntry *TextureCacheCommon::SetTexture() {
|
||||
entry->status = TexCacheEntry::STATUS_UNRELIABLE;
|
||||
}
|
||||
|
||||
if (hasClutGPU) {
|
||||
WARN_LOG_REPORT_ONCE(clutUseRender, G3D, "Using texture with dynamic CLUT: texfmt=%d, clutfmt=%d", gstate.getTextureFormat(), gstate.getClutPaletteFormat());
|
||||
entry->status |= TexCacheEntry::STATUS_CLUT_GPU;
|
||||
}
|
||||
|
||||
if (hasClut && clutRenderAddress_ == 0xFFFFFFFF) {
|
||||
const u64 cachekeyMin = (u64)(texaddr & 0x3FFFFFFF) << 32;
|
||||
const u64 cachekeyMax = cachekeyMin + (1ULL << 32);
|
||||
@@ -947,12 +964,13 @@ bool TextureCacheCommon::MatchFramebuffer(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check works for D16 too (???)
|
||||
// Check works for D16 too.
|
||||
const bool matchingClutFormat =
|
||||
(fb_format == GE_FORMAT_DEPTH16 && entry.format == GE_TFMT_CLUT16) ||
|
||||
(fb_format == GE_FORMAT_DEPTH16 && entry.format == GE_TFMT_5650) ||
|
||||
(fb_format == GE_FORMAT_8888 && entry.format == GE_TFMT_CLUT32) ||
|
||||
(fb_format != GE_FORMAT_8888 && entry.format == GE_TFMT_CLUT16);
|
||||
(fb_format != GE_FORMAT_8888 && entry.format == GE_TFMT_CLUT16) ||
|
||||
(fb_format == GE_FORMAT_8888 && entry.format == GE_TFMT_CLUT8);
|
||||
|
||||
const int texBitsPerPixel = std::max(1U, (u32)textureBitsPerPixel[entry.format]);
|
||||
const int byteOffset = texaddr - addr;
|
||||
@@ -991,6 +1009,10 @@ bool TextureCacheCommon::MatchFramebuffer(
|
||||
|
||||
if (fb_stride_in_bytes != tex_stride_in_bytes) {
|
||||
// Probably irrelevant. Although, as we shall see soon, there are exceptions.
|
||||
// Burnout Dominator lens flare trick special case.
|
||||
if (fb_format == GE_FORMAT_8888 && entry.format == GE_TFMT_CLUT8 && texWidth == 4 && texHeight == 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1017,8 +1039,8 @@ bool TextureCacheCommon::MatchFramebuffer(
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
WARN_LOG_ONCE(diffFormat2, G3D, "Ignoring possible texturing from framebuffer with incompatible format %s != %s at %08x",
|
||||
GeTextureFormatToString(entry.format), GeBufferFormatToString(fb_format), fb_address);
|
||||
WARN_LOG_ONCE(diffFormat2, G3D, "Ignoring possible texturing from framebuffer with incompatible format %s != %s at %08x (+%dx%d)",
|
||||
GeTextureFormatToString(entry.format), GeBufferFormatToString(fb_format), fb_address, matchInfo->xOffset, matchInfo->yOffset);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1168,33 +1190,71 @@ void TextureCacheCommon::LoadClut(u32 clutAddr, u32 loadBytes) {
|
||||
if (Memory::IsValidAddress(clutAddr)) {
|
||||
if (Memory::IsVRAMAddress(clutAddr)) {
|
||||
// Clear the uncached bit, etc. to match framebuffers.
|
||||
const u32 clutFramebufAddr = clutAddr & 0x3FFFFFFF;
|
||||
const u32 clutFramebufEnd = clutFramebufAddr + loadBytes;
|
||||
const u32 clutLoadAddr = clutAddr & 0x3FFFFFFF;
|
||||
const u32 clutLoadEnd = clutLoadAddr + loadBytes;
|
||||
static const u32 MAX_CLUT_OFFSET = 4096;
|
||||
|
||||
clutRenderOffset_ = MAX_CLUT_OFFSET;
|
||||
const std::vector<VirtualFramebuffer *> &framebuffers = framebufferManager_->Framebuffers();
|
||||
|
||||
VirtualFramebuffer *chosenFramebuffer = nullptr;
|
||||
for (VirtualFramebuffer *framebuffer : framebuffers) {
|
||||
const u32 fb_address = framebuffer->fb_address & 0x3FFFFFFF;
|
||||
const u32 bpp = BufferFormatBytesPerPixel(framebuffer->fb_format);
|
||||
u32 offset = clutFramebufAddr - fb_address;
|
||||
const u32 fb_bpp = BufferFormatBytesPerPixel(framebuffer->fb_format);
|
||||
int offset = clutLoadAddr - fb_address;
|
||||
|
||||
// Is this inside the framebuffer at all?
|
||||
bool matchRange = fb_address + framebuffer->fb_stride * bpp > clutFramebufAddr && fb_address < clutFramebufEnd;
|
||||
// And is it inside the rendered area? Sometimes games pack data outside.
|
||||
bool matchRegion = ((offset / bpp) % framebuffer->fb_stride) < framebuffer->width;
|
||||
if (matchRange && matchRegion && offset < clutRenderOffset_) {
|
||||
WARN_LOG_N_TIMES(clutfb, 5, G3D, "Detected LoadCLUT(%d bytes) from framebuffer %08x (%s), byte offset %d", loadBytes, fb_address, GeBufferFormatToString(framebuffer->fb_format), offset);
|
||||
framebuffer->last_frame_clut = gpuStats.numFlips;
|
||||
framebuffer->usageFlags |= FB_USAGE_CLUT;
|
||||
clutRenderAddress_ = framebuffer->fb_address;
|
||||
clutRenderOffset_ = offset;
|
||||
if (offset == 0) {
|
||||
break;
|
||||
// Is this inside the framebuffer at all? Note that we only check the first line here, this should
|
||||
// be changed.
|
||||
bool matchRange = offset >= 0 && offset < (int)(framebuffer->fb_stride * fb_bpp);
|
||||
if (matchRange) {
|
||||
// And is it inside the rendered area? Sometimes games pack data in the margin between width and stride.
|
||||
// If the framebuffer width was detected as 512, we're gonna assume it's really 480.
|
||||
int fbMatchWidth = framebuffer->width;
|
||||
if (fbMatchWidth == 512) {
|
||||
fbMatchWidth = 480;
|
||||
}
|
||||
bool inMargin = ((offset / fb_bpp) % framebuffer->fb_stride) == fbMatchWidth;
|
||||
|
||||
// The offset check here means, in the context of the loop, that we'll pick
|
||||
// the framebuffer with the smallest offset. This is yet another framebuffer matching
|
||||
// loop with its own rules, eventually we'll probably want to do something
|
||||
// more systematic.
|
||||
if (matchRange && !inMargin && offset < (int)clutRenderOffset_) {
|
||||
WARN_LOG_N_TIMES(clutfb, 5, G3D, "Detected LoadCLUT(%d bytes) from framebuffer %08x (%s), byte offset %d", loadBytes, fb_address, GeBufferFormatToString(framebuffer->fb_format), offset);
|
||||
framebuffer->last_frame_clut = gpuStats.numFlips;
|
||||
framebuffer->usageFlags |= FB_USAGE_CLUT;
|
||||
clutRenderAddress_ = framebuffer->fb_address;
|
||||
clutRenderOffset_ = (u32)offset;
|
||||
chosenFramebuffer = framebuffer;
|
||||
if (offset == 0) {
|
||||
// Not gonna find a better match according to the smallest-offset rule, so we'll go with this one.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chosenFramebuffer) {
|
||||
if (!dynamicClutTemp_) {
|
||||
Draw::FramebufferDesc desc{};
|
||||
desc.width = 512;
|
||||
desc.height = 1;
|
||||
desc.depth = 1;
|
||||
desc.z_stencil = false;
|
||||
desc.numColorAttachments = 1;
|
||||
desc.tag = "dynamic_clut";
|
||||
dynamicClutFbo_ = draw_->CreateFramebuffer(desc);
|
||||
desc.tag = "dynamic_clut_temp";
|
||||
dynamicClutTemp_ = draw_->CreateFramebuffer(desc);
|
||||
}
|
||||
|
||||
// Download the pixels to our temp clut, scaling down if needed.
|
||||
framebufferManager_->BlitUsingRaster(
|
||||
chosenFramebuffer->fbo, 0.0f, 0.0f, 512.0f * chosenFramebuffer->renderScaleFactor, 1.0f,
|
||||
dynamicClutTemp_, 0.0f, 0.0f, 512.0f, 1.0f,
|
||||
false, 1.0f, framebufferManager_->Get2DPipeline(DRAW2D_COPY_COLOR), "copy_clut_to_temp");
|
||||
clutRenderFormat_ = chosenFramebuffer->fb_format;
|
||||
}
|
||||
NotifyMemInfo(MemBlockFlags::ALLOC, clutAddr, loadBytes, "CLUT");
|
||||
}
|
||||
|
||||
@@ -1471,10 +1531,29 @@ inline u32 TfmtRawToFullAlpha(GETextureFormat fmt) {
|
||||
}
|
||||
}
|
||||
|
||||
CheckAlphaResult TextureCacheCommon::DecodeTextureLevel(u8 *out, int outPitch, GETextureFormat format, GEPaletteFormat clutformat, uint32_t texaddr, int level, int bufw, bool reverseColors, bool expandTo32bit) {
|
||||
// Used for converting CLUT4 to CLUT8.
|
||||
// Could SIMD or whatever, though will hardly be a bottleneck.
|
||||
static void Expand4To8Bits(u8 *dest, const u8 *src, int srcWidth) {
|
||||
for (int i = 0; i < (srcWidth + 1) / 2; i++) {
|
||||
u8 lower = src[i] & 0xF;
|
||||
u8 upper = src[i] >> 4;
|
||||
dest[i * 2] = lower;
|
||||
dest[i * 2 + 1] = upper;
|
||||
}
|
||||
}
|
||||
|
||||
CheckAlphaResult TextureCacheCommon::DecodeTextureLevel(u8 *out, int outPitch, GETextureFormat format, GEPaletteFormat clutformat, uint32_t texaddr, int level, int bufw, TexDecodeFlags flags) {
|
||||
u32 alphaSum = 0xFFFFFFFF;
|
||||
u32 fullAlphaMask = 0x0;
|
||||
|
||||
bool expandTo32bit = (flags & TexDecodeFlags::EXPAND32) != 0;
|
||||
bool reverseColors = (flags & TexDecodeFlags::REVERSE_COLORS) != 0;
|
||||
bool toClut8 = (flags & TexDecodeFlags::TO_CLUT8) != 0;
|
||||
|
||||
if (toClut8 && format != GE_TFMT_CLUT8 && format != GE_TFMT_CLUT4) {
|
||||
_dbg_assert_(false);
|
||||
}
|
||||
|
||||
bool swizzled = gstate.isTextureSwizzled();
|
||||
if ((texaddr & 0x00600000) != 0 && Memory::IsVRAMAddress(texaddr)) {
|
||||
// This means it's in a mirror, possibly a swizzled mirror. Let's report.
|
||||
@@ -1508,6 +1587,15 @@ CheckAlphaResult TextureCacheCommon::DecodeTextureLevel(u8 *out, int outPitch, G
|
||||
texptr = (u8 *)tmpTexBuf32_.data();
|
||||
}
|
||||
|
||||
if (toClut8) {
|
||||
// We just need to expand from 4 to 8 bits.
|
||||
for (int y = 0; y < h; ++y) {
|
||||
Expand4To8Bits((u8 *)out + outPitch * y, texptr + (bufw * y) / 2, w);
|
||||
}
|
||||
// We can't know anything about alpha.
|
||||
return CHECKALPHA_ANY;
|
||||
}
|
||||
|
||||
switch (clutformat) {
|
||||
case GE_CMODE_16BIT_BGR5650:
|
||||
case GE_CMODE_16BIT_ABGR5551:
|
||||
@@ -1570,6 +1658,19 @@ CheckAlphaResult TextureCacheCommon::DecodeTextureLevel(u8 *out, int outPitch, G
|
||||
break;
|
||||
|
||||
case GE_TFMT_CLUT8:
|
||||
if (toClut8) {
|
||||
if (gstate.isTextureSwizzled()) {
|
||||
tmpTexBuf32_.resize(bufw * ((h + 7) & ~7));
|
||||
UnswizzleFromMem(tmpTexBuf32_.data(), bufw, texptr, bufw, h, 1);
|
||||
texptr = (u8 *)tmpTexBuf32_.data();
|
||||
}
|
||||
// After deswizzling, we are in the correct format and can just copy.
|
||||
for (int y = 0; y < h; ++y) {
|
||||
memcpy((u8 *)out + outPitch * y, texptr + (bufw * y), w);
|
||||
}
|
||||
// We can't know anything about alpha.
|
||||
return CHECKALPHA_ANY;
|
||||
}
|
||||
return ReadIndexedTex(out, outPitch, level, texptr, 1, bufw, reverseColors, expandTo32bit);
|
||||
|
||||
case GE_TFMT_CLUT16:
|
||||
@@ -1855,10 +1956,18 @@ void TextureCacheCommon::ApplyTexture() {
|
||||
InvalidateLastTexture();
|
||||
}
|
||||
|
||||
entry->lastFrame = gpuStats.numFlips;
|
||||
BindTexture(entry);
|
||||
gstate_c.SetTextureFullAlpha(entry->GetAlphaStatus() == TexCacheEntry::STATUS_ALPHA_FULL);
|
||||
gstate_c.SetTextureIs3D((entry->status & TexCacheEntry::STATUS_3D) != 0);
|
||||
if (entry->status & TexCacheEntry::STATUS_CLUT_GPU) {
|
||||
// Special process.
|
||||
ApplyTextureDepal(entry);
|
||||
entry->lastFrame = gpuStats.numFlips;
|
||||
gstate_c.SetTextureFullAlpha(false);
|
||||
gstate_c.SetTextureIs3D(false);
|
||||
} else {
|
||||
entry->lastFrame = gpuStats.numFlips;
|
||||
BindTexture(entry);
|
||||
gstate_c.SetTextureFullAlpha(entry->GetAlphaStatus() == TexCacheEntry::STATUS_ALPHA_FULL);
|
||||
gstate_c.SetTextureIs3D((entry->status & TexCacheEntry::STATUS_3D) != 0);
|
||||
}
|
||||
}
|
||||
|
||||
static bool CanDepalettize(GETextureFormat texFormat, GEBufferFormat bufferFormat) {
|
||||
@@ -1873,7 +1982,7 @@ static bool CanDepalettize(GETextureFormat texFormat, GEBufferFormat bufferForma
|
||||
}
|
||||
break;
|
||||
case GE_FORMAT_8888:
|
||||
if (texFormat == GE_TFMT_CLUT32) {
|
||||
if (texFormat == GE_TFMT_CLUT32 || texFormat == GE_TFMT_CLUT8) { // clut8 takes a special depal mode.
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
@@ -1915,7 +2024,6 @@ static bool CanUseSmoothDepal(const GPUgstate &gstate, GEBufferFormat framebuffe
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
void TextureCacheCommon::ApplyTextureFramebuffer(VirtualFramebuffer *framebuffer, GETextureFormat texFormat, RasterChannel channel) {
|
||||
Draw2DPipeline *textureShader = nullptr;
|
||||
uint32_t clutMode = gstate.clutformat & 0xFFFFFF;
|
||||
@@ -1961,9 +2069,17 @@ void TextureCacheCommon::ApplyTextureFramebuffer(VirtualFramebuffer *framebuffer
|
||||
samplerKey.mipEnable = false;
|
||||
ApplySamplingParams(samplerKey);
|
||||
|
||||
ShaderDepalMode mode = ShaderDepalMode::NORMAL;
|
||||
if (texFormat == GE_TFMT_CLUT8 && framebuffer->fb_format == GE_FORMAT_8888) {
|
||||
mode = ShaderDepalMode::CLUT8_8888;
|
||||
smoothedDepal = false; // just in case
|
||||
} else if (smoothedDepal) {
|
||||
mode = ShaderDepalMode::SMOOTHED;
|
||||
}
|
||||
|
||||
// Since we started/ended render passes, might need these.
|
||||
gstate_c.Dirty(DIRTY_DEPAL);
|
||||
gstate_c.SetUseShaderDepal(smoothedDepal ? ShaderDepalMode::SMOOTHED : ShaderDepalMode::NORMAL);
|
||||
gstate_c.SetUseShaderDepal(mode);
|
||||
gstate_c.depalFramebufferFormat = framebuffer->fb_format;
|
||||
|
||||
const u32 bytesPerColor = clutFormat == GE_CMODE_32BIT_ABGR8888 ? sizeof(u32) : sizeof(u16);
|
||||
@@ -2063,6 +2179,98 @@ void TextureCacheCommon::ApplyTextureFramebuffer(VirtualFramebuffer *framebuffer
|
||||
gstate_c.Dirty(DIRTY_BLEND_STATE | DIRTY_DEPTHSTENCIL_STATE | DIRTY_RASTER_STATE | DIRTY_VIEWPORTSCISSOR_STATE);
|
||||
}
|
||||
|
||||
// Applies depal to a normal (non-framebuffer) texture, pre-decoded to CLUT8 format.
|
||||
void TextureCacheCommon::ApplyTextureDepal(TexCacheEntry *entry) {
|
||||
uint32_t clutMode = gstate.clutformat & 0xFFFFFF;
|
||||
|
||||
switch (entry->format) {
|
||||
case GE_TFMT_CLUT4:
|
||||
case GE_TFMT_CLUT8:
|
||||
break; // These are OK
|
||||
default:
|
||||
_dbg_assert_(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const GEPaletteFormat clutFormat = gstate.getClutPaletteFormat();
|
||||
u32 depthUpperBits = 0;
|
||||
|
||||
// The CLUT texture is dynamic, it's the framebuffer pointed to by clutRenderAddress.
|
||||
// Instead of texturing directly from that, we copy to a temporary CLUT texture.
|
||||
GEBufferFormat expectedCLUTBufferFormat = (GEBufferFormat)clutFormat; // All entries from clutFormat correspond directly to buffer formats.
|
||||
|
||||
// OK, figure out what format we want our framebuffer in, so it can be reinterpreted if needed.
|
||||
// If no reinterpretation is needed, we'll automatically just get a copy shader.
|
||||
float scaleFactorX = 1.0f;
|
||||
Draw2DPipeline *reinterpret = framebufferManager_->GetReinterpretPipeline(clutRenderFormat_, expectedCLUTBufferFormat, &scaleFactorX);
|
||||
framebufferManager_->BlitUsingRaster(
|
||||
dynamicClutTemp_, 0.0f, 0.0f, 512.0f, 1.0f, dynamicClutFbo_, 0.0f, 0.0f, scaleFactorX * 512.0f, 1.0f, false, 1.0f, reinterpret, "reinterpret_clut");
|
||||
|
||||
Draw2DPipeline *textureShader = textureShaderCache_->GetDepalettizeShader(clutMode, GE_TFMT_CLUT8, GE_FORMAT_CLUT8, false, 0);
|
||||
gstate_c.SetUseShaderDepal(ShaderDepalMode::OFF);
|
||||
|
||||
int texWidth = gstate.getTextureWidth(0);
|
||||
int texHeight = gstate.getTextureHeight(0);
|
||||
|
||||
// If min is not < max, then we don't have values (wasn't set during decode.)
|
||||
const KnownVertexBounds &bounds = gstate_c.vertBounds;
|
||||
float u1 = 0.0f;
|
||||
float v1 = 0.0f;
|
||||
float u2 = texWidth;
|
||||
float v2 = texHeight;
|
||||
if (bounds.minV < bounds.maxV) {
|
||||
u1 = (bounds.minU + gstate_c.curTextureXOffset) * texWidth;
|
||||
v1 = (bounds.minV + gstate_c.curTextureYOffset) * texHeight;
|
||||
u2 = (bounds.maxU + gstate_c.curTextureXOffset) * texWidth;
|
||||
v2 = (bounds.maxV + gstate_c.curTextureYOffset) * texHeight;
|
||||
// We need to reapply the texture next time since we cropped UV.
|
||||
gstate_c.Dirty(DIRTY_TEXTURE_PARAMS);
|
||||
}
|
||||
|
||||
Draw::Framebuffer *depalFBO = framebufferManager_->GetTempFBO(TempFBO::DEPAL, texWidth, texHeight);
|
||||
draw_->BindTexture(0, nullptr);
|
||||
draw_->BindTexture(1, nullptr);
|
||||
draw_->BindFramebufferAsRenderTarget(depalFBO, { Draw::RPAction::DONT_CARE, Draw::RPAction::DONT_CARE, Draw::RPAction::DONT_CARE }, "Depal");
|
||||
draw_->InvalidateFramebuffer(Draw::FB_INVALIDATION_STORE, Draw::FB_DEPTH_BIT | Draw::FB_STENCIL_BIT);
|
||||
draw_->SetScissorRect(u1, v1, u2 - u1, v2 - v1);
|
||||
Draw::Viewport vp{ 0.0f, 0.0f, (float)texWidth, (float)texHeight, 0.0f, 1.0f };
|
||||
draw_->SetViewports(1, &vp);
|
||||
|
||||
draw_->BindNativeTexture(0, GetNativeTextureView(entry));
|
||||
draw_->BindFramebufferAsTexture(dynamicClutFbo_, 1, Draw::FB_COLOR_BIT, 0);
|
||||
Draw::SamplerState *nearest = textureShaderCache_->GetSampler(false);
|
||||
Draw::SamplerState *clutSampler = textureShaderCache_->GetSampler(false);
|
||||
draw_->BindSamplerStates(0, 1, &nearest);
|
||||
draw_->BindSamplerStates(1, 1, &clutSampler);
|
||||
|
||||
draw2D_->Blit(textureShader, u1, v1, u2, v2, u1, v1, u2, v2, texWidth, texHeight, texWidth, texHeight, false, 1);
|
||||
|
||||
gpuStats.numDepal++;
|
||||
|
||||
gstate_c.curTextureWidth = texWidth;
|
||||
|
||||
draw_->BindTexture(0, nullptr);
|
||||
framebufferManager_->RebindFramebuffer("ApplyTextureFramebuffer");
|
||||
|
||||
draw_->BindFramebufferAsTexture(depalFBO, 0, Draw::FB_COLOR_BIT, 0);
|
||||
BoundFramebufferTexture();
|
||||
|
||||
const u32 bytesPerColor = clutFormat == GE_CMODE_32BIT_ABGR8888 ? sizeof(u32) : sizeof(u16);
|
||||
const u32 clutTotalColors = clutMaxBytes_ / bytesPerColor;
|
||||
|
||||
// We don't know about alpha at all.
|
||||
gstate_c.SetTextureFullAlpha(false);
|
||||
|
||||
draw_->InvalidateCachedState();
|
||||
shaderManager_->DirtyLastShader();
|
||||
|
||||
SamplerCacheKey samplerKey = GetFramebufferSamplingParams(texWidth, texHeight);
|
||||
ApplySamplingParams(samplerKey);
|
||||
|
||||
// Since we started/ended render passes, might need these.
|
||||
gstate_c.Dirty(DIRTY_BLEND_STATE | DIRTY_DEPTHSTENCIL_STATE | DIRTY_RASTER_STATE | DIRTY_VIEWPORTSCISSOR_STATE);
|
||||
}
|
||||
|
||||
void TextureCacheCommon::Clear(bool delete_them) {
|
||||
textureShaderCache_->Clear();
|
||||
|
||||
@@ -2082,6 +2290,15 @@ void TextureCacheCommon::Clear(bool delete_them) {
|
||||
secondCacheSizeEstimate_ = 0;
|
||||
}
|
||||
videos_.clear();
|
||||
|
||||
if (dynamicClutFbo_) {
|
||||
dynamicClutFbo_->Release();
|
||||
dynamicClutFbo_ = nullptr;
|
||||
}
|
||||
if (dynamicClutTemp_) {
|
||||
dynamicClutTemp_->Release();
|
||||
dynamicClutTemp_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void TextureCacheCommon::DeleteTexture(TexCache::iterator it) {
|
||||
@@ -2448,6 +2665,21 @@ bool TextureCacheCommon::PrepareBuildTexture(BuildTexturePlan &plan, TexCacheEnt
|
||||
plan.maxPossibleLevels = log2i(std::min(plan.createW, plan.createH)) + 1;
|
||||
}
|
||||
|
||||
if (entry->status & TexCacheEntry::TexStatus::STATUS_CLUT_GPU) {
|
||||
_dbg_assert_(entry->format == GE_TFMT_CLUT4 || entry->format == GE_TFMT_CLUT8);
|
||||
plan.decodeToClut8 = true;
|
||||
// We only support 1 mip level when doing CLUT on GPU for now.
|
||||
// Supporting more would be possible, just not very interesting until we need it.
|
||||
plan.levelsToCreate = 1;
|
||||
plan.levelsToLoad = 1;
|
||||
plan.maxPossibleLevels = 1;
|
||||
plan.scaleFactor = 1;
|
||||
plan.saveTexture = false; // Can't yet save these properly.
|
||||
// TODO: Also forcibly disable replacement, or check that the replacement is a 8-bit paletted texture.
|
||||
} else {
|
||||
plan.decodeToClut8 = false;
|
||||
}
|
||||
|
||||
if (plan.levelsToCreate == 1) {
|
||||
entry->status |= TexCacheEntry::STATUS_NO_MIPS;
|
||||
} else {
|
||||
@@ -2459,7 +2691,7 @@ bool TextureCacheCommon::PrepareBuildTexture(BuildTexturePlan &plan, TexCacheEnt
|
||||
return true;
|
||||
}
|
||||
|
||||
void TextureCacheCommon::LoadTextureLevel(TexCacheEntry &entry, uint8_t *data, int stride, ReplacedTexture &replaced, int srcLevel, int scaleFactor, Draw::DataFormat dstFmt, bool reverseColors) {
|
||||
void TextureCacheCommon::LoadTextureLevel(TexCacheEntry &entry, uint8_t *data, int stride, ReplacedTexture &replaced, int srcLevel, int scaleFactor, Draw::DataFormat dstFmt, TexDecodeFlags texDecFlags) {
|
||||
int w = gstate.getTextureWidth(srcLevel);
|
||||
int h = gstate.getTextureHeight(srcLevel);
|
||||
|
||||
@@ -2486,9 +2718,14 @@ void TextureCacheCommon::LoadTextureLevel(TexCacheEntry &entry, uint8_t *data, i
|
||||
decPitch = stride;
|
||||
}
|
||||
|
||||
bool expand32 = !gstate_c.Supports(GPU_SUPPORTS_16BIT_FORMATS) || dstFmt == Draw::DataFormat::R8G8B8A8_UNORM;
|
||||
if (!gstate_c.Supports(GPU_SUPPORTS_16BIT_FORMATS) || dstFmt == Draw::DataFormat::R8G8B8A8_UNORM) {
|
||||
texDecFlags |= TexDecodeFlags::EXPAND32;
|
||||
}
|
||||
if (entry.status & TexCacheEntry::STATUS_CLUT_GPU) {
|
||||
texDecFlags |= TexDecodeFlags::TO_CLUT8;
|
||||
}
|
||||
|
||||
CheckAlphaResult alphaResult = DecodeTextureLevel((u8 *)pixelData, decPitch, tfmt, clutformat, texaddr, srcLevel, bufw, reverseColors, expand32);
|
||||
CheckAlphaResult alphaResult = DecodeTextureLevel((u8 *)pixelData, decPitch, tfmt, clutformat, texaddr, srcLevel, bufw, texDecFlags);
|
||||
entry.SetAlphaStatus(alphaResult, srcLevel);
|
||||
|
||||
if (scaleFactor > 1) {
|
||||
|
||||
@@ -50,6 +50,13 @@ struct VirtualFramebuffer;
|
||||
class TextureReplacer;
|
||||
class ShaderManagerCommon;
|
||||
|
||||
enum class TexDecodeFlags {
|
||||
EXPAND32 = 1,
|
||||
REVERSE_COLORS = 2,
|
||||
TO_CLUT8 = 4,
|
||||
};
|
||||
ENUM_CLASS_BITOPS(TexDecodeFlags);
|
||||
|
||||
namespace Draw {
|
||||
class DrawContext;
|
||||
class Texture;
|
||||
@@ -102,6 +109,8 @@ struct TextureDefinition {
|
||||
|
||||
// NOTE: These only handle textures loaded directly from PSP memory contents.
|
||||
// Framebuffer textures do not have entries, we bind the framebuffers directly.
|
||||
// At one point we might merge the concepts of framebuffers and textures, but that
|
||||
// moment is far away.
|
||||
struct TexCacheEntry {
|
||||
~TexCacheEntry() {
|
||||
if (texturePtr || textureName || vkTex)
|
||||
@@ -138,6 +147,8 @@ struct TexCacheEntry {
|
||||
STATUS_FORCE_REBUILD = 0x2000,
|
||||
|
||||
STATUS_3D = 0x4000,
|
||||
|
||||
STATUS_CLUT_GPU = 0x8000,
|
||||
};
|
||||
|
||||
// Status, but int so we can zero initialize.
|
||||
@@ -275,6 +286,9 @@ struct BuildTexturePlan {
|
||||
bool replaceValid;
|
||||
bool saveTexture;
|
||||
|
||||
// TODO: Expand32 should probably also be decided in PrepareBuildTexture.
|
||||
bool decodeToClut8;
|
||||
|
||||
void GetMipSize(int level, int *w, int *h) const {
|
||||
if (replaceValid) {
|
||||
replaced->GetSize(level, *w, *h);
|
||||
@@ -337,6 +351,7 @@ public:
|
||||
virtual bool GetCurrentTextureDebug(GPUDebugBuffer &buffer, int level) { return false; }
|
||||
|
||||
protected:
|
||||
virtual void *GetNativeTextureView(const TexCacheEntry *entry) = 0;
|
||||
bool PrepareBuildTexture(BuildTexturePlan &plan, TexCacheEntry *entry);
|
||||
|
||||
virtual void BindTexture(TexCacheEntry *entry) = 0;
|
||||
@@ -346,6 +361,7 @@ protected:
|
||||
void Decimate(bool forcePressure = false);
|
||||
|
||||
void ApplyTextureFramebuffer(VirtualFramebuffer *framebuffer, GETextureFormat texFormat, RasterChannel channel);
|
||||
void ApplyTextureDepal(TexCacheEntry *entry);
|
||||
|
||||
void HandleTextureChange(TexCacheEntry *const entry, const char *reason, bool initialMatch, bool doDelete);
|
||||
virtual void BuildTexture(TexCacheEntry *const entry) = 0;
|
||||
@@ -354,13 +370,13 @@ protected:
|
||||
|
||||
virtual void BindAsClutTexture(Draw::Texture *tex, bool smooth) {}
|
||||
|
||||
CheckAlphaResult DecodeTextureLevel(u8 *out, int outPitch, GETextureFormat format, GEPaletteFormat clutformat, uint32_t texaddr, int level, int bufw, bool reverseColors, bool expandTo32Bit);
|
||||
CheckAlphaResult DecodeTextureLevel(u8 *out, int outPitch, GETextureFormat format, GEPaletteFormat clutformat, uint32_t texaddr, int level, int bufw, TexDecodeFlags flags);
|
||||
void UnswizzleFromMem(u32 *dest, u32 destPitch, const u8 *texptr, u32 bufw, u32 height, u32 bytesPerPixel);
|
||||
CheckAlphaResult ReadIndexedTex(u8 *out, int outPitch, int level, const u8 *texptr, int bytesPerIndex, int bufw, bool reverseColors, bool expandTo32Bit);
|
||||
ReplacedTexture &FindReplacement(TexCacheEntry *entry, int &w, int &h, int &d);
|
||||
|
||||
// Return value is mapData normally, but could be another buffer allocated with AllocateAlignedMemory.
|
||||
void LoadTextureLevel(TexCacheEntry &entry, uint8_t *mapData, int mapRowPitch, ReplacedTexture &replaced, int srcLevel, int scaleFactor, Draw::DataFormat dstFmt, bool reverseColors);
|
||||
void LoadTextureLevel(TexCacheEntry &entry, uint8_t *mapData, int mapRowPitch, ReplacedTexture &replaced, int srcLevel, int scaleFactor, Draw::DataFormat dstFmt, TexDecodeFlags texDecFlags);
|
||||
|
||||
template <typename T>
|
||||
inline const T *GetCurrentClut() {
|
||||
@@ -470,10 +486,16 @@ protected:
|
||||
u32 clutMaxBytes_ = 0;
|
||||
u32 clutRenderAddress_ = 0xFFFFFFFF;
|
||||
u32 clutRenderOffset_;
|
||||
GEBufferFormat clutRenderFormat_;
|
||||
|
||||
// True if the clut is just alpha values in the same order (RGBA4444-bit only.)
|
||||
bool clutAlphaLinear_ = false;
|
||||
u16 clutAlphaLinearColor_;
|
||||
|
||||
// Facilities for GPU depal of static textures.
|
||||
Draw::Framebuffer *dynamicClutTemp_ = nullptr;
|
||||
Draw::Framebuffer *dynamicClutFbo_ = nullptr;
|
||||
|
||||
int standardScaleFactor_;
|
||||
int shaderScaleFactor_ = 0;
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ ClutTexture TextureShaderCache::GetClutTexture(GEPaletteFormat clutFormat, const
|
||||
int lastA = 0;
|
||||
|
||||
int rampLength = 0;
|
||||
// Quick check for how many continouosly growing entries we have at the start.
|
||||
// Quick check for how many continuously growing entries we have at the start.
|
||||
// Bilinearly filtering CLUTs only really makes sense for this kind of ramp.
|
||||
for (int i = 0; i < maxClutEntries; i++) {
|
||||
rampLength = i;
|
||||
|
||||
@@ -65,6 +65,8 @@ Draw::DataFormat FromD3D11Format(u32 fmt) {
|
||||
return Draw::DataFormat::A1R5G5B5_UNORM_PACK16;
|
||||
case DXGI_FORMAT_B5G6R5_UNORM:
|
||||
return Draw::DataFormat::R5G6B5_UNORM_PACK16;
|
||||
case DXGI_FORMAT_R8_UNORM:
|
||||
return Draw::DataFormat::R8_UNORM;
|
||||
case DXGI_FORMAT_B8G8R8A8_UNORM:
|
||||
default:
|
||||
return Draw::DataFormat::R8G8B8A8_UNORM;
|
||||
@@ -290,6 +292,8 @@ void TextureCacheD3D11::BuildTexture(TexCacheEntry *const entry) {
|
||||
dstFmt = ToDXGIFormat(plan.replaced->Format(plan.baseLevelSrc));
|
||||
} else if (plan.scaleFactor > 1 || plan.saveTexture) {
|
||||
dstFmt = DXGI_FORMAT_B8G8R8A8_UNORM;
|
||||
} else if (plan.decodeToClut8) {
|
||||
dstFmt = DXGI_FORMAT_R8_UNORM;
|
||||
}
|
||||
|
||||
int levels;
|
||||
@@ -378,7 +382,7 @@ void TextureCacheD3D11::BuildTexture(TexCacheEntry *const entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
LoadTextureLevel(*entry, data, stride, *plan.replaced, srcLevel, plan.scaleFactor, texFmt, false);
|
||||
LoadTextureLevel(*entry, data, stride, *plan.replaced, srcLevel, plan.scaleFactor, texFmt, TexDecodeFlags{});
|
||||
if (plan.depth == 1) {
|
||||
context_->UpdateSubresource(texture, i, nullptr, data, stride, 0);
|
||||
} else {
|
||||
@@ -525,3 +529,8 @@ bool TextureCacheD3D11::GetCurrentTextureDebug(GPUDebugBuffer &buffer, int level
|
||||
stagingCopy->Release();
|
||||
return true;
|
||||
}
|
||||
|
||||
void *TextureCacheD3D11::GetNativeTextureView(const TexCacheEntry *entry) {
|
||||
ID3D11ShaderResourceView *textureView = DxView(entry);
|
||||
return (void *)textureView;
|
||||
}
|
||||
@@ -61,6 +61,7 @@ protected:
|
||||
void ReleaseTexture(TexCacheEntry *entry, bool delete_them) override;
|
||||
void BindAsClutTexture(Draw::Texture *tex, bool smooth) override;
|
||||
void ApplySamplingParams(const SamplerCacheKey &key) override;
|
||||
void *GetNativeTextureView(const TexCacheEntry *entry) override;
|
||||
|
||||
private:
|
||||
DXGI_FORMAT GetDestFormat(GETextureFormat format, GEPaletteFormat clutFormat) const;
|
||||
@@ -72,10 +73,10 @@ private:
|
||||
ID3D11Device *device_;
|
||||
ID3D11DeviceContext *context_;
|
||||
|
||||
ID3D11Resource *&DxTex(TexCacheEntry *entry) {
|
||||
ID3D11Resource *&DxTex(const TexCacheEntry *entry) {
|
||||
return (ID3D11Resource *&)entry->texturePtr;
|
||||
}
|
||||
ID3D11ShaderResourceView *DxView(TexCacheEntry *entry) {
|
||||
ID3D11ShaderResourceView *DxView(const TexCacheEntry *entry) {
|
||||
return (ID3D11ShaderResourceView *)entry->textureView;
|
||||
}
|
||||
|
||||
|
||||
@@ -290,8 +290,6 @@ void TextureCacheDX9::BuildTexture(TexCacheEntry *const entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
Draw::DataFormat texFmt = FromD3D9Format(dstFmt);
|
||||
|
||||
if (plan.depth == 1) {
|
||||
// Regular loop.
|
||||
for (int i = 0; i < levels; i++) {
|
||||
@@ -307,7 +305,7 @@ void TextureCacheDX9::BuildTexture(TexCacheEntry *const entry) {
|
||||
}
|
||||
uint8_t *data = (uint8_t *)rect.pBits;
|
||||
int stride = rect.Pitch;
|
||||
LoadTextureLevel(*entry, data, stride, *plan.replaced, (i == 0) ? plan.baseLevelSrc : i, plan.scaleFactor, texFmt, false);
|
||||
LoadTextureLevel(*entry, data, stride, *plan.replaced, (i == 0) ? plan.baseLevelSrc : i, plan.scaleFactor, FromD3D9Format(dstFmt), TexDecodeFlags{});
|
||||
((LPDIRECT3DTEXTURE9)texture)->UnlockRect(dstLevel);
|
||||
}
|
||||
} else {
|
||||
@@ -322,7 +320,7 @@ void TextureCacheDX9::BuildTexture(TexCacheEntry *const entry) {
|
||||
uint8_t *data = (uint8_t *)box.pBits;
|
||||
int stride = box.RowPitch;
|
||||
for (int i = 0; i < plan.depth; i++) {
|
||||
LoadTextureLevel(*entry, data, stride, *plan.replaced, (i == 0) ? plan.baseLevelSrc : i, plan.scaleFactor, texFmt, false);
|
||||
LoadTextureLevel(*entry, data, stride, *plan.replaced, (i == 0) ? plan.baseLevelSrc : i, plan.scaleFactor, FromD3D9Format(dstFmt), TexDecodeFlags{});
|
||||
data += box.SlicePitch;
|
||||
}
|
||||
((LPDIRECT3DVOLUMETEXTURE9)texture)->UnlockBox(0);
|
||||
@@ -456,3 +454,8 @@ bool TextureCacheDX9::GetCurrentTextureDebug(GPUDebugBuffer &buffer, int level)
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
void *TextureCacheDX9::GetNativeTextureView(const TexCacheEntry *entry) {
|
||||
LPDIRECT3DBASETEXTURE9 tex = DxTex(entry);
|
||||
return (void *)tex;
|
||||
}
|
||||
@@ -50,6 +50,7 @@ protected:
|
||||
void Unbind() override;
|
||||
void ReleaseTexture(TexCacheEntry *entry, bool delete_them) override;
|
||||
void BindAsClutTexture(Draw::Texture *tex, bool smooth) override;
|
||||
void *GetNativeTextureView(const TexCacheEntry *entry) override;
|
||||
|
||||
private:
|
||||
void ApplySamplingParams(const SamplerCacheKey &key) override;
|
||||
@@ -60,7 +61,7 @@ private:
|
||||
|
||||
void BuildTexture(TexCacheEntry *const entry) override;
|
||||
|
||||
LPDIRECT3DBASETEXTURE9 &DxTex(TexCacheEntry *entry) {
|
||||
LPDIRECT3DBASETEXTURE9 &DxTex(const TexCacheEntry *entry) const {
|
||||
return *(LPDIRECT3DBASETEXTURE9 *)&entry->texturePtr;
|
||||
}
|
||||
|
||||
|
||||
@@ -266,6 +266,8 @@ void TextureCacheGLES::BuildTexture(TexCacheEntry *const entry) {
|
||||
dstFmt = plan.replaced->Format(plan.baseLevelSrc);
|
||||
} else if (plan.scaleFactor > 1 || plan.saveTexture) {
|
||||
dstFmt = Draw::DataFormat::R8G8B8A8_UNORM;
|
||||
} else if (plan.decodeToClut8) {
|
||||
dstFmt = Draw::DataFormat::R8_UNORM;
|
||||
}
|
||||
|
||||
if (plan.depth == 1) {
|
||||
@@ -313,7 +315,7 @@ void TextureCacheGLES::BuildTexture(TexCacheEntry *const entry) {
|
||||
if (plan.scaleFactor > 1) {
|
||||
bpp = 4;
|
||||
} else {
|
||||
bpp = dstFmt == Draw::DataFormat::R8G8B8A8_UNORM ? 4 : 2;
|
||||
bpp = (int)Draw::DataFormatSizeInBytes(dstFmt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +327,7 @@ void TextureCacheGLES::BuildTexture(TexCacheEntry *const entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
LoadTextureLevel(*entry, data, stride, *plan.replaced, srcLevel, plan.scaleFactor, dstFmt, true);
|
||||
LoadTextureLevel(*entry, data, stride, *plan.replaced, srcLevel, plan.scaleFactor, dstFmt, TexDecodeFlags::REVERSE_COLORS);
|
||||
|
||||
// NOTE: TextureImage takes ownership of data, so we don't free it afterwards.
|
||||
render_->TextureImage(entry->textureName, i, mipWidth, mipHeight, 1, dstFmt, data, GLRAllocType::ALIGNED);
|
||||
@@ -335,7 +337,7 @@ void TextureCacheGLES::BuildTexture(TexCacheEntry *const entry) {
|
||||
|
||||
render_->FinalizeTexture(entry->textureName, plan.levelsToLoad, genMips);
|
||||
} else {
|
||||
int bpp = dstFmt == Draw::DataFormat::R8G8B8A8_UNORM ? 4 : 2;
|
||||
int bpp = (int)Draw::DataFormatSizeInBytes(dstFmt);
|
||||
int stride = bpp * (plan.w * plan.scaleFactor);
|
||||
int levelStride = stride * (plan.h * plan.scaleFactor);
|
||||
|
||||
@@ -344,7 +346,7 @@ void TextureCacheGLES::BuildTexture(TexCacheEntry *const entry) {
|
||||
u8 *p = data;
|
||||
|
||||
for (int i = 0; i < plan.depth; i++) {
|
||||
LoadTextureLevel(*entry, p, stride, *plan.replaced, i, plan.scaleFactor, dstFmt, true);
|
||||
LoadTextureLevel(*entry, p, stride, *plan.replaced, i, plan.scaleFactor, dstFmt, TexDecodeFlags::REVERSE_COLORS);
|
||||
p += levelStride;
|
||||
}
|
||||
|
||||
@@ -443,3 +445,8 @@ void TextureCacheGLES::DeviceRestore(Draw::DrawContext *draw) {
|
||||
render_ = (GLRenderManager *)draw_->GetNativeObject(Draw::NativeObject::RENDER_MANAGER);
|
||||
textureShaderCache_->DeviceRestore(draw);
|
||||
}
|
||||
|
||||
void *TextureCacheGLES::GetNativeTextureView(const TexCacheEntry *entry) {
|
||||
GLRTexture *tex = entry->textureName;
|
||||
return (void *)tex;
|
||||
}
|
||||
@@ -66,6 +66,7 @@ protected:
|
||||
void ReleaseTexture(TexCacheEntry *entry, bool delete_them) override;
|
||||
|
||||
void BindAsClutTexture(Draw::Texture *tex, bool smooth) override;
|
||||
void *GetNativeTextureView(const TexCacheEntry *entry) override;
|
||||
|
||||
private:
|
||||
void ApplySamplingParams(const SamplerCacheKey &key) override;
|
||||
|
||||
@@ -107,6 +107,15 @@ void main() {
|
||||
|
||||
)";
|
||||
|
||||
static int VkFormatBytesPerPixel(VkFormat format) {
|
||||
switch (format) {
|
||||
case VULKAN_8888_FORMAT: return 4;
|
||||
case VULKAN_CLUT8_FORMAT: return 1;
|
||||
default: break;
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
||||
SamplerCache::~SamplerCache() {
|
||||
DeviceLost();
|
||||
}
|
||||
@@ -448,6 +457,8 @@ void TextureCacheVulkan::BuildTexture(TexCacheEntry *const entry) {
|
||||
if (plan.scaleFactor > 1) {
|
||||
// Whether hardware or software scaling, this is the dest format.
|
||||
dstFmt = VULKAN_8888_FORMAT;
|
||||
} else if (plan.decodeToClut8) {
|
||||
dstFmt = VULKAN_CLUT8_FORMAT;
|
||||
}
|
||||
|
||||
// We don't generate mipmaps for 512x512 textures because they're almost exclusively used for menu backgrounds
|
||||
@@ -479,7 +490,7 @@ void TextureCacheVulkan::BuildTexture(TexCacheEntry *const entry) {
|
||||
case VULKAN_4444_FORMAT: mapping = &VULKAN_4444_SWIZZLE; break;
|
||||
case VULKAN_1555_FORMAT: mapping = &VULKAN_1555_SWIZZLE; break;
|
||||
case VULKAN_565_FORMAT: mapping = &VULKAN_565_SWIZZLE; break;
|
||||
default: mapping = &VULKAN_8888_SWIZZLE; break;
|
||||
default: mapping = &VULKAN_8888_SWIZZLE; break; // no swizzle
|
||||
}
|
||||
|
||||
VkImageLayout imageLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
|
||||
@@ -562,7 +573,7 @@ void TextureCacheVulkan::BuildTexture(TexCacheEntry *const entry) {
|
||||
int mipHeight;
|
||||
plan.GetMipSize(i, &mipWidth, &mipHeight);
|
||||
|
||||
int bpp = actualFmt == VULKAN_8888_FORMAT ? 4 : 2; // output bpp
|
||||
int bpp = VkFormatBytesPerPixel(actualFmt);
|
||||
int stride = (mipWidth * bpp + 15) & ~15; // output stride
|
||||
int uploadSize = stride * mipHeight;
|
||||
|
||||
@@ -602,7 +613,7 @@ void TextureCacheVulkan::BuildTexture(TexCacheEntry *const entry) {
|
||||
loadLevel(uploadSize, i, stride, plan.scaleFactor);
|
||||
entry->vkTex->UploadMip(cmdInit, 0, mipWidth, mipHeight, i, texBuf, bufferOffset, stride / bpp);
|
||||
} else if (computeUpload) {
|
||||
int srcBpp = dstFmt == VULKAN_8888_FORMAT ? 4 : 2;
|
||||
int srcBpp = VkFormatBytesPerPixel(dstFmt);
|
||||
int srcStride = mipUnscaledWidth * srcBpp;
|
||||
int srcSize = srcStride * mipUnscaledHeight;
|
||||
loadLevel(srcSize, i == 0 ? plan.baseLevelSrc : i, srcStride, 1);
|
||||
@@ -723,12 +734,18 @@ void TextureCacheVulkan::LoadTextureLevel(TexCacheEntry &entry, uint8_t *writePt
|
||||
_assert_msg_(texaddr != 0, "Can't load a texture from address null")
|
||||
|
||||
int bufw = GetTextureBufw(level, texaddr, tfmt);
|
||||
int bpp = dstFmt == VULKAN_8888_FORMAT ? 4 : 2;
|
||||
int bpp = VkFormatBytesPerPixel(dstFmt);
|
||||
|
||||
u32 *pixelData;
|
||||
int decPitch;
|
||||
|
||||
bool expand32 = !gstate_c.Supports(GPU_SUPPORTS_16BIT_FORMATS) || scaleFactor > 1 || dstFmt == VULKAN_8888_FORMAT;
|
||||
TexDecodeFlags texDecFlags{};
|
||||
if (!gstate_c.Supports(GPU_SUPPORTS_16BIT_FORMATS) || scaleFactor > 1 || dstFmt == VULKAN_8888_FORMAT) {
|
||||
texDecFlags |= TexDecodeFlags::EXPAND32;
|
||||
}
|
||||
if (entry.status & TexCacheEntry::STATUS_CLUT_GPU) {
|
||||
texDecFlags |= TexDecodeFlags::TO_CLUT8;
|
||||
}
|
||||
|
||||
if (scaleFactor > 1) {
|
||||
tmpTexBufRearrange_.resize(std::max(bufw, w) * h);
|
||||
@@ -740,7 +757,7 @@ void TextureCacheVulkan::LoadTextureLevel(TexCacheEntry &entry, uint8_t *writePt
|
||||
decPitch = rowPitch;
|
||||
}
|
||||
|
||||
CheckAlphaResult alphaResult = DecodeTextureLevel((u8 *)pixelData, decPitch, tfmt, clutformat, texaddr, level, bufw, false, expand32);
|
||||
CheckAlphaResult alphaResult = DecodeTextureLevel((u8 *)pixelData, decPitch, tfmt, clutformat, texaddr, level, bufw, texDecFlags);
|
||||
entry.SetAlphaStatus(alphaResult, level);
|
||||
|
||||
if (scaleFactor > 1) {
|
||||
@@ -854,3 +871,8 @@ std::vector<std::string> TextureCacheVulkan::DebugGetSamplerIDs() const {
|
||||
std::string TextureCacheVulkan::DebugGetSamplerString(std::string id, DebugShaderStringType stringType) {
|
||||
return samplerCache_.DebugGetSamplerString(id, stringType);
|
||||
}
|
||||
|
||||
void *TextureCacheVulkan::GetNativeTextureView(const TexCacheEntry *entry) {
|
||||
VkImageView view = entry->vkTex->GetImageView();
|
||||
return (void *)view;
|
||||
}
|
||||
@@ -97,6 +97,7 @@ protected:
|
||||
void BindAsClutTexture(Draw::Texture *tex, bool smooth) override;
|
||||
void ApplySamplingParams(const SamplerCacheKey &key) override;
|
||||
void BoundFramebufferTexture() override;
|
||||
void *GetNativeTextureView(const TexCacheEntry *entry) override;
|
||||
|
||||
private:
|
||||
void LoadTextureLevel(TexCacheEntry &entry, uint8_t *writePtr, int rowPitch, int level, int scaleFactor, VkFormat dstFmt);
|
||||
|
||||
@@ -36,6 +36,7 @@ extern const VkComponentMapping VULKAN_8888_SWIZZLE;
|
||||
#define VULKAN_1555_FORMAT VK_FORMAT_A1R5G5B5_UNORM_PACK16
|
||||
#define VULKAN_565_FORMAT VK_FORMAT_B5G6R5_UNORM_PACK16 // TODO: Does not actually have mandatory support, though R5G6B5 does! See #14602
|
||||
#define VULKAN_8888_FORMAT VK_FORMAT_R8G8B8A8_UNORM
|
||||
#define VULKAN_CLUT8_FORMAT VK_FORMAT_R8_UNORM
|
||||
|
||||
// Manager for compute shaders that upload things (and those have two bindings: a storage buffer to read from and an image to write to).
|
||||
class VulkanComputeShaderManager {
|
||||
|
||||
+36
-29
@@ -278,20 +278,6 @@ enum GECommand {
|
||||
GE_CMD_NOP_FF = 0xFF,
|
||||
};
|
||||
|
||||
enum GEBufferFormat : uint8_t {
|
||||
GE_FORMAT_565 = 0,
|
||||
GE_FORMAT_5551 = 1,
|
||||
GE_FORMAT_4444 = 2,
|
||||
GE_FORMAT_8888 = 3,
|
||||
GE_FORMAT_DEPTH16 = 4, // Virtual format, just used to pass into Depal
|
||||
GE_FORMAT_INVALID = 0xFF,
|
||||
};
|
||||
|
||||
const char *GeBufferFormatToString(GEBufferFormat fmt);
|
||||
inline bool IsGeBufferFormat16BitColor(GEBufferFormat fmt) {
|
||||
return (int)fmt < 3;
|
||||
}
|
||||
|
||||
#define GE_VTYPE_TRANSFORM (0<<23)
|
||||
#define GE_VTYPE_THROUGH (1<<23)
|
||||
#define GE_VTYPE_THROUGH_MASK (1<<23)
|
||||
@@ -413,8 +399,10 @@ enum GELightComputation
|
||||
GE_LIGHTCOMP_ONLYPOWDIFFUSE = 2,
|
||||
};
|
||||
|
||||
enum GETextureFormat : uint8_t
|
||||
{
|
||||
// TODO: Consolidate the below three to one enum? The first four are the same in all,
|
||||
// and we sometimes need to interpret back and forth between them.
|
||||
|
||||
enum GETextureFormat : uint8_t {
|
||||
GE_TFMT_5650 = 0,
|
||||
GE_TFMT_5551 = 1,
|
||||
GE_TFMT_4444 = 2,
|
||||
@@ -428,6 +416,32 @@ enum GETextureFormat : uint8_t
|
||||
GE_TFMT_DXT5 = 10,
|
||||
};
|
||||
|
||||
enum GEBufferFormat : uint8_t {
|
||||
GE_FORMAT_565 = 0,
|
||||
GE_FORMAT_5551 = 1,
|
||||
GE_FORMAT_4444 = 2,
|
||||
GE_FORMAT_8888 = 3,
|
||||
GE_FORMAT_DEPTH16 = 4, // Virtual format, just used to pass into Depal
|
||||
GE_FORMAT_CLUT8 = 5, // Virtual format, for pre-decoded static textures with dynamic CLUT
|
||||
GE_FORMAT_INVALID = 0xFF,
|
||||
};
|
||||
|
||||
enum GEPaletteFormat : uint8_t {
|
||||
GE_CMODE_16BIT_BGR5650,
|
||||
GE_CMODE_16BIT_ABGR5551,
|
||||
GE_CMODE_16BIT_ABGR4444,
|
||||
GE_CMODE_32BIT_ABGR8888,
|
||||
};
|
||||
|
||||
const char *GEPaletteFormatToString(GEPaletteFormat pfmt);
|
||||
const char *GeTextureFormatToString(GETextureFormat tfmt);
|
||||
const char *GeTextureFormatToString(GETextureFormat tfmt, GEPaletteFormat pfmt);
|
||||
|
||||
const char *GeBufferFormatToString(GEBufferFormat fmt);
|
||||
inline bool IsGeBufferFormat16BitColor(GEBufferFormat fmt) {
|
||||
return (int)fmt < 3;
|
||||
}
|
||||
|
||||
inline bool IsClutFormat(GETextureFormat tfmt) {
|
||||
return tfmt == GE_TFMT_CLUT4 || tfmt == GE_TFMT_CLUT8 || tfmt == GE_TFMT_CLUT16 || tfmt == GE_TFMT_CLUT32;
|
||||
}
|
||||
@@ -445,7 +459,12 @@ inline bool IsTextureFormat16Bit(GETextureFormat tfmt) {
|
||||
}
|
||||
|
||||
inline int BufferFormatBytesPerPixel(GEBufferFormat format) {
|
||||
return format == GE_FORMAT_8888 ? 4 : 2; // applies to depth as well.
|
||||
switch (format) {
|
||||
case GE_FORMAT_8888: return 4; // applies to depth as well.
|
||||
case GE_FORMAT_CLUT8: return 1;
|
||||
default:
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool TextureFormatMatchesBufferFormat(GETextureFormat fmt, GEBufferFormat bfmt) {
|
||||
@@ -613,15 +632,3 @@ inline GEPrimitiveType PatchPrimToPrim(GEPatchPrimType type) {
|
||||
case GE_PATCHPRIM_UNKNOWN: default: return GE_PRIM_POINTS; // Treated as points.
|
||||
}
|
||||
}
|
||||
|
||||
enum GEPaletteFormat
|
||||
{
|
||||
GE_CMODE_16BIT_BGR5650,
|
||||
GE_CMODE_16BIT_ABGR5551,
|
||||
GE_CMODE_16BIT_ABGR4444,
|
||||
GE_CMODE_32BIT_ABGR8888,
|
||||
};
|
||||
|
||||
const char *GEPaletteFormatToString(GEPaletteFormat pfmt);
|
||||
const char *GeTextureFormatToString(GETextureFormat tfmt);
|
||||
const char *GeTextureFormatToString(GETextureFormat tfmt, GEPaletteFormat pfmt);
|
||||
+38
-1
@@ -1152,6 +1152,17 @@ ULES00262 = true
|
||||
ULUS10064 = true
|
||||
ULKS46087 = true
|
||||
|
||||
# Burnout Dominator - lens flare effect (issue #11100)
|
||||
ULUS10236 = true
|
||||
ULES00703 = true
|
||||
|
||||
# Need for Speed - Shift (same as Burnout Dominator)
|
||||
ULUS10462 = true
|
||||
ULES01275 = true
|
||||
ULJM05494 = true
|
||||
NPJH50143 = true
|
||||
ULJM05738 = true
|
||||
|
||||
[DisableFirstFrameReadback]
|
||||
# Wipeout Pure: Temporary workaround for lens flare flicker. See #13344
|
||||
UCUS98612 = true
|
||||
@@ -1300,5 +1311,31 @@ ULES00262 = true
|
||||
ULUS10064 = true
|
||||
ULKS46087 = true
|
||||
|
||||
# Burnout Dominator - lens flare effect (issue #11100)
|
||||
# Some of the steps don't work at high resolution yet.
|
||||
ULUS10236 = true
|
||||
ULES00703 = true
|
||||
|
||||
# Need for Speed - Shift (same as Burnout Dominator)
|
||||
ULUS10462 = true
|
||||
ULES01275 = true
|
||||
ULJM05494 = true
|
||||
NPJH50143 = true
|
||||
ULJM05738 = true
|
||||
|
||||
[AllowDownloadCLUT]
|
||||
# Temporary compatibility option, while developing a GPU CLUT-from-framebuffer path.
|
||||
# Temporary compatibility option, while working on the GPU CLUT-from-framebuffer path.
|
||||
# Not required for any games now that it works, but might be useful for development.
|
||||
|
||||
[UploadDepthForCLUTTextures]
|
||||
# Burnout Dominator - lens flare effect (issue #11100)
|
||||
# We need a preinitialized depth buffer
|
||||
ULUS10236 = true
|
||||
ULES00703 = true
|
||||
|
||||
# Need for Speed - Shift (same as Burnout Dominator)
|
||||
ULUS10462 = true
|
||||
ULES01275 = true
|
||||
ULJM05494 = true
|
||||
NPJH50143 = true
|
||||
ULJM05738 = true
|
||||
Reference in new issue
Block a user