mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-16 07:50:28 +02:00
Merge pull request #22465 from hrydgard/hw-ge-blends
HW backends: Improve accuracy of blending
This commit is contained in:
17 files changed
+86
-25
No files matched your search
@@ -933,7 +933,7 @@ void VulkanQueueRunner::LogRenderPass(const VKRStep &pass, bool verbose) {
|
||||
INFO_LOG(Log::G3D, " BindGraphicsPipeline(%x)", (int)(intptr_t)cmd.graphics_pipeline.pipeline);
|
||||
break;
|
||||
case VKRRenderCommand::BLEND:
|
||||
INFO_LOG(Log::G3D, " BlendColor(%08x)", cmd.blendColor.color);
|
||||
INFO_LOG(Log::G3D, " BlendColor(%f, %f, %f, %f)", cmd.blendColor.color[0], cmd.blendColor.color[1], cmd.blendColor.color[2], cmd.blendColor.color[3]);
|
||||
break;
|
||||
case VKRRenderCommand::CLEAR:
|
||||
INFO_LOG(Log::G3D, " Clear");
|
||||
@@ -1179,12 +1179,8 @@ void VulkanQueueRunner::PerformRenderPass(const VKRStep &step, VkCommandBuffer c
|
||||
}
|
||||
|
||||
case VKRRenderCommand::BLEND:
|
||||
{
|
||||
float bc[4];
|
||||
Uint8x4ToFloat4(bc, c.blendColor.color);
|
||||
vkCmdSetBlendConstants(cmd, bc);
|
||||
vkCmdSetBlendConstants(cmd, c.blendColor.color);
|
||||
break;
|
||||
}
|
||||
|
||||
case VKRRenderCommand::PUSH_CONSTANTS:
|
||||
if (pipelineOK) {
|
||||
|
||||
@@ -99,7 +99,7 @@ struct VkRenderData {
|
||||
uint8_t stencilRef;
|
||||
} stencil;
|
||||
struct {
|
||||
uint32_t color;
|
||||
float color[4];
|
||||
} blendColor;
|
||||
struct {
|
||||
VkShaderStageFlags stages;
|
||||
|
||||
@@ -396,11 +396,11 @@ public:
|
||||
data.stencil.stencilRef = refValue;
|
||||
}
|
||||
|
||||
void SetBlendFactor(uint32_t color) {
|
||||
void SetBlendFactor(const float color[4]) {
|
||||
_dbg_assert_(curRenderStep_ && curRenderStep_->stepType == VKRStepType::RENDER);
|
||||
VkRenderData &data = curRenderStep_->commands.push_uninitialized();
|
||||
data.cmd = VKRRenderCommand::BLEND;
|
||||
data.blendColor.color = color;
|
||||
memcpy(data.blendColor.color, color, sizeof(data.blendColor.color));
|
||||
}
|
||||
|
||||
void PushConstants(VkShaderStageFlags stages, int offset, int size, void *constants) {
|
||||
|
||||
@@ -1370,8 +1370,7 @@ void VKContext::SetViewport(const Viewport &viewport) {
|
||||
}
|
||||
|
||||
void VKContext::SetBlendFactor(float color[4]) {
|
||||
uint32_t col = Float4ToUint8x4(color);
|
||||
renderManager_.SetBlendFactor(col);
|
||||
renderManager_.SetBlendFactor(color);
|
||||
}
|
||||
|
||||
void VKContext::SetStencilParams(uint8_t refValue, uint8_t writeMask, uint8_t compareMask) {
|
||||
|
||||
@@ -205,6 +205,7 @@ static const CommandLineParam g_autoParams[] = {
|
||||
{POFF(screenshotFilename), CmdParamType::String, "screenshot", '\0', "Compare rendered output against a reference screenshot FILE", CmdLineMode::Headless},
|
||||
{POFF(screenshotFilenameSave), CmdParamType::String, "screenshot-save", '\0', "Save rendered screenshot to specified path (PNG if the path ends in .png, BMP otherwise)", CmdLineMode::Headless},
|
||||
{POFF(replayEnd), CmdParamType::Int, "replay-end", '\0', "When replaying a GE frame dump, only draw primitives up to this one (1-based)", CmdLineMode::Headless},
|
||||
{POFF(dumpCompat), CmdParamType::Bool, "dump-compat", '\0', "Apply the game's compat.ini settings when replaying a GE frame dump (off by default)", CmdLineMode::Headless},
|
||||
{POFF(depthFilenameSave), CmdParamType::String, "depth-save", '\0', "Save the depth buffer to FILE along with the screenshot (u32 width, u32 height, u16 depths; float depths if FILE ends in .f32)", CmdLineMode::Headless},
|
||||
{POFF(screenshotRenderTarget), CmdParamType::Bool, "screenshot-render-target", '\0', "Take screenshots of the current render target instead of the displayed framebuffer", CmdLineMode::Headless},
|
||||
{POFF(screenshotFilenameDiff), CmdParamType::String, "screenshot-diff", '\0', "Save a visual comparison image to FILE when comparing screenshots", CmdLineMode::Headless},
|
||||
|
||||
@@ -178,6 +178,8 @@ struct CommandLineOptions {
|
||||
std::optional<std::string> screenshotFilenameSave;
|
||||
std::optional<std::string> depthFilenameSave;
|
||||
std::optional<int> replayEnd;
|
||||
// Headless: apply the game's compatibility settings when replaying a GE frame dump.
|
||||
std::optional<bool> dumpCompat;
|
||||
std::optional<bool> screenshotRenderTarget;
|
||||
std::optional<std::string> screenshotFilenameDiff;
|
||||
// Headless: preserve the alpha channel when saving PNG screenshots.
|
||||
|
||||
@@ -75,6 +75,8 @@ struct CoreParameter {
|
||||
bool startBreak = false;
|
||||
std::string *collectDebugOutput = nullptr;
|
||||
bool headLess = false; // Try to avoid messageboxes etc
|
||||
// Headless leaves out compat settings when replaying frame dumps, unless this is set.
|
||||
bool dumpCompat = false;
|
||||
|
||||
// Internal PSP rendering resolution and scale factor.
|
||||
int renderScaleFactor = 1;
|
||||
|
||||
+1
-1
@@ -498,7 +498,7 @@ static bool CPU_Init(FileLoader *fileLoader, IdentifiedFileType type, std::strin
|
||||
// Homebrew get fake disc IDs assigned to the global paramSFO, so they shouldn't clash with real games.
|
||||
|
||||
std::string discId = g_paramSFO.GetDiscID();
|
||||
if (type == IdentifiedFileType::PPSSPP_GE_DUMP && g_CoreParameter.headLess) {
|
||||
if (type == IdentifiedFileType::PPSSPP_GE_DUMP && g_CoreParameter.headLess && !g_CoreParameter.dumpCompat) {
|
||||
// Headless replays frame dumps to compare them with references and with the PSP, which a game's
|
||||
// workarounds would skew: DarkStalkers' present hack replaces the game's final upscale.
|
||||
g_CoreParameter.compat.Clear();
|
||||
|
||||
@@ -1008,7 +1008,21 @@ bool GenerateFragmentShader(const FShaderID &id, char *buffer, const ShaderLangu
|
||||
return false;
|
||||
}
|
||||
|
||||
WRITE(p, " v.rgb = v.rgb * %s;\n", srcFactor);
|
||||
if (replaceBlend == REPLACE_BLEND_PRE_SRC && replaceBlendFuncA == GE_SRCBLEND_FIXA && IsGEExactFixedBlend(replaceBlendFuncB, replaceBlendEq)) {
|
||||
// The GE's product of a color and a factor is floor((2c+1)(2f+1)/1024), and the hardware blend multiplies
|
||||
// the dst by (2B+1)/512 (see ConvertBlendState). Lowering the src term by 0.5 - (2B+1)/1024 turns the
|
||||
// rounding of the sum into the GE's floor, except where that would go below zero.
|
||||
// The color is truncated too: the GE's bilinear filter truncates (4-bit weights, see the software
|
||||
// Sampler), and rounding the GPU's finer filtered value instead makes each texel about a level
|
||||
// brighter. That doubles God Eater Burst's faint bloom, which feeds back into itself pass after pass.
|
||||
// The 0.01 keeps an unfiltered texel's exact integer from flooring a level down on float error.
|
||||
WRITE(p, " vec3 fixA2 = floor(u_blendFixA * 510.0 + 0.5) + 1.0;\n");
|
||||
WRITE(p, " vec3 fixB2 = floor(u_blendFixB * 510.0 + 0.5) + 1.0;\n");
|
||||
WRITE(p, " v.rgb = floor((floor(v.rgb * 255.0 + 0.01) * 2.0 + 1.0) * fixA2 / 1024.0);\n");
|
||||
WRITE(p, " v.rgb = (v.rgb - 0.5 + fixB2 / 1024.0) / 255.0;\n");
|
||||
} else {
|
||||
WRITE(p, " v.rgb = v.rgb * %s;\n", srcFactor);
|
||||
}
|
||||
}
|
||||
|
||||
if (replaceBlend == REPLACE_BLEND_READ_FRAMEBUFFER) {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
#include "Common/Data/Convert/SmallDataConvert.h"
|
||||
#include "Core/ConfigValues.h"
|
||||
#include "Core/System.h"
|
||||
#include "Core/Config.h"
|
||||
@@ -262,6 +263,16 @@ StencilValueType ReplaceAlphaWithStencilType() {
|
||||
return STENCIL_VALUE_KEEP;
|
||||
}
|
||||
|
||||
void GenericBlendState::blendColorToFloat4(float out[4]) const {
|
||||
Uint8x4ToFloat4(out, blendColor);
|
||||
if (blendColorGE) {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
const int c = (blendColor >> (i * 8)) & 0xFF;
|
||||
out[i] = (2 * c + 1) / 512.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReplaceBlendType ReplaceBlendWithShader(GEBufferFormat bufferFormat) {
|
||||
if (gstate_c.blueToAlpha) {
|
||||
return REPLACE_BLEND_BLUE_TO_ALPHA;
|
||||
@@ -324,6 +335,10 @@ ReplaceBlendType ReplaceBlendWithShader(GEBufferFormat bufferFormat) {
|
||||
// Using a copy isn't accurate either, though, when there's overlap.
|
||||
if (gstate_c.Use(GPU_USE_FRAMEBUFFER_FETCH))
|
||||
return REPLACE_BLEND_READ_FRAMEBUFFER;
|
||||
// Through mode is mostly full screen passes that don't overlap themselves, so a copy is safe there.
|
||||
// Super Stardust composites its bloom at alpha 0xFF, doubling the dst, which 2x alpha clamps to 1x.
|
||||
if (gstate.isModeThrough())
|
||||
return REPLACE_BLEND_READ_FRAMEBUFFER;
|
||||
return REPLACE_BLEND_PRE_SRC_2X_ALPHA;
|
||||
|
||||
case GE_DSTBLEND_DOUBLEINVSRCALPHA:
|
||||
@@ -450,6 +465,11 @@ ReplaceBlendType ReplaceBlendWithShader(GEBufferFormat bufferFormat) {
|
||||
if (gstate.getFixA() == 0xFFFFFF && gstate.getFixB() == 0x000000) {
|
||||
// Some games specify this. Some GPUs may prefer blending off entirely.
|
||||
return REPLACE_BLEND_NO;
|
||||
} else if (IsGEExactFixedBlend(funcB, gstate.getBlendEq())) {
|
||||
// 0 and 0xFFFFFF are exact as blend factors. Other fixed colors need the GE's math, see ConvertBlendState.
|
||||
const bool trivialA = gstate.getFixA() == 0xFFFFFF || gstate.getFixA() == 0x000000;
|
||||
const bool trivialB = gstate.getFixB() == 0xFFFFFF || gstate.getFixB() == 0x000000;
|
||||
return trivialA && trivialB ? REPLACE_BLEND_STANDARD : REPLACE_BLEND_PRE_SRC;
|
||||
} else if (gstate.getFixA() == 0xFFFFFF || gstate.getFixA() == 0x000000 || gstate.getFixB() == 0xFFFFFF || gstate.getFixB() == 0x000000) {
|
||||
// We can represent this with standard factors.
|
||||
return REPLACE_BLEND_STANDARD;
|
||||
@@ -488,8 +508,12 @@ ReplaceBlendType ReplaceBlendWithShader(GEBufferFormat bufferFormat) {
|
||||
// We will just hope that doubling alpha for the dst factor will not clamp too badly.
|
||||
if (gstate_c.Use(GPU_USE_FRAMEBUFFER_FETCH))
|
||||
return REPLACE_BLEND_READ_FRAMEBUFFER;
|
||||
// Through mode is mostly full screen passes that don't overlap themselves, so a copy is safe there.
|
||||
// MotorStorm brightens its frame with DSTCOLOR + DOUBLESRCALPHA at alpha 0xFE, which is up to 3x the dst,
|
||||
// and doubling alpha in the shader clamps that to 2x.
|
||||
if (gstate.isModeThrough())
|
||||
return REPLACE_BLEND_READ_FRAMEBUFFER;
|
||||
// Hm, this is similar to the L.A. Rush case above. This will not be accurate.
|
||||
// Wonder in which games we encounter this? One example is MotorStorm.
|
||||
return REPLACE_BLEND_2X_ALPHA;
|
||||
}
|
||||
|
||||
@@ -1068,7 +1092,19 @@ static void ConvertBlendState(GenericBlendState &blendState, FBReadSetting useFB
|
||||
glBlendFuncB = toDualSource(glBlendFuncB);
|
||||
}
|
||||
|
||||
if (blendFuncA == GE_SRCBLEND_FIXA || blendFuncB == GE_DSTBLEND_FIXB) {
|
||||
if (usePreSrc && replaceBlend == REPLACE_BLEND_PRE_SRC && blendFuncA == GE_SRCBLEND_FIXA && IsGEExactFixedBlend(blendFuncB, blendFuncEq)) {
|
||||
// The shader computed the src term like the GE and lowered it so that the hardware's rounding of the
|
||||
// sum lands on the GE's floor of it, given the dst factor (2B+1)/512 the GE multiplies with.
|
||||
if (fixB == 0 || fixB == 0xFFFFFF) {
|
||||
glBlendFuncB = fixB == 0 ? BlendFactor::ZERO : BlendFactor::ONE;
|
||||
if (constantAlphaGL == BlendFactor::CONSTANT_ALPHA) {
|
||||
blendState.defaultBlendColor(constantAlpha);
|
||||
}
|
||||
} else {
|
||||
glBlendFuncB = BlendFactor::CONSTANT_COLOR;
|
||||
blendState.setBlendColorGE(fixB, constantAlpha);
|
||||
}
|
||||
} else if (blendFuncA == GE_SRCBLEND_FIXA || blendFuncB == GE_DSTBLEND_FIXB) {
|
||||
if (glBlendFuncA == BlendFactor::INVALID && glBlendFuncB != BlendFactor::INVALID) {
|
||||
// Can use blendcolor trivially.
|
||||
blendState.setBlendColor(fixA, constantAlpha);
|
||||
|
||||
@@ -68,6 +68,11 @@ int ClearModeChannelMask();
|
||||
ReplaceAlphaType ReplaceAlphaWithStencil(ReplaceBlendType replaceBlend);
|
||||
ReplaceBlendType ReplaceBlendWithShader(GEBufferFormat bufferFormat);
|
||||
|
||||
// Fixed color blends (with FIXA as the src factor) that REPLACE_BLEND_PRE_SRC computes like the GE.
|
||||
inline bool IsGEExactFixedBlend(GEBlendDstFactor funcB, GEBlendMode eq) {
|
||||
return funcB == GE_DSTBLEND_FIXB && eq == GE_BLENDMODE_MUL_AND_ADD;
|
||||
}
|
||||
|
||||
// This is for the fallback path if real logic ops are not available.
|
||||
SimulateLogicOpType SimulateLogicOpShaderTypeIfNeeded();
|
||||
|
||||
@@ -143,6 +148,8 @@ struct GenericBlendState {
|
||||
|
||||
bool useBlendColor;
|
||||
u32 blendColor;
|
||||
// The color channels are a GE fixed blend factor, applied as (2c+1)/512 like the GE does.
|
||||
bool blendColorGE;
|
||||
|
||||
void setFactors(BlendFactor srcC, BlendFactor dstC, BlendFactor srcA, BlendFactor dstA) {
|
||||
srcColor = srcC;
|
||||
@@ -157,11 +164,16 @@ struct GenericBlendState {
|
||||
void setBlendColor(uint32_t color, uint8_t alpha) {
|
||||
blendColor = color | ((uint32_t)alpha << 24);
|
||||
useBlendColor = true;
|
||||
blendColorGE = false;
|
||||
}
|
||||
void setBlendColorGE(uint32_t color, uint8_t alpha) {
|
||||
setBlendColor(color, alpha);
|
||||
blendColorGE = true;
|
||||
}
|
||||
void defaultBlendColor(uint8_t alpha) {
|
||||
blendColor = 0xFFFFFF | ((uint32_t)alpha << 24);
|
||||
useBlendColor = true;
|
||||
setBlendColor(0xFFFFFF, alpha);
|
||||
}
|
||||
void blendColorToFloat4(float out[4]) const;
|
||||
|
||||
void Log();
|
||||
};
|
||||
|
||||
@@ -192,7 +192,7 @@ void DrawEngineD3D11::ApplyDrawState(int prim) {
|
||||
}
|
||||
dynState_.useBlendColor = blendState.useBlendColor;
|
||||
if (blendState.useBlendColor) {
|
||||
dynState_.blendColor = blendState.blendColor;
|
||||
blendState.blendColorToFloat4(dynState_.blendColor);
|
||||
}
|
||||
} else {
|
||||
keys_.blend.blendEnable = false;
|
||||
@@ -437,8 +437,7 @@ void DrawEngineD3D11::ApplyDrawStateLate(bool applyStencilRef, uint8_t stencilRe
|
||||
}
|
||||
if (gstate_c.IsDirty(DIRTY_BLEND_STATE)) {
|
||||
// Need to do this AFTER ApplyTexture because the process of depalettization can ruin the blend state.
|
||||
float blendColor[4];
|
||||
Uint8x4ToFloat4(blendColor, dynState_.blendColor);
|
||||
const float *blendColor = dynState_.blendColor;
|
||||
if (device1_) {
|
||||
context1_->OMSetBlendState(blendState1_, blendColor, 0xFFFFFFFF);
|
||||
} else {
|
||||
|
||||
@@ -65,7 +65,7 @@ struct D3D11StateKeys {
|
||||
struct D3D11DynamicState {
|
||||
int topology;
|
||||
bool useBlendColor;
|
||||
uint32_t blendColor;
|
||||
float blendColor[4];
|
||||
bool useStencil;
|
||||
uint8_t stencilRef;
|
||||
Draw::Viewport viewport;
|
||||
|
||||
@@ -180,9 +180,8 @@ void DrawEngineGLES::ApplyDrawState(int prim) {
|
||||
gstate_c.Dirty(DIRTY_SHADERBLEND);
|
||||
}
|
||||
if (blendState.useBlendColor) {
|
||||
uint32_t color = blendState.blendColor;
|
||||
float col[4];
|
||||
Uint8x4ToFloat4(col, color);
|
||||
blendState.blendColorToFloat4(col);
|
||||
renderManager->SetBlendFactor(col);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ void DrawEngineVulkan::ConvertStateToVulkanKey(FramebufferManagerVulkan &fbManag
|
||||
}
|
||||
dynState.useBlendColor = blendState.useBlendColor;
|
||||
if (blendState.useBlendColor) {
|
||||
dynState.blendColor = blendState.blendColor;
|
||||
blendState.blendColorToFloat4(dynState.blendColor);
|
||||
}
|
||||
} else {
|
||||
key.blendEnable = false;
|
||||
|
||||
@@ -15,7 +15,7 @@ struct VulkanDynamicState {
|
||||
VkViewport viewport;
|
||||
ScissorRect scissor;
|
||||
bool useBlendColor;
|
||||
uint32_t blendColor;
|
||||
float blendColor[4];
|
||||
bool useStencil;
|
||||
uint8_t stencilRef;
|
||||
uint8_t stencilWriteMask;
|
||||
|
||||
@@ -1070,6 +1070,7 @@ int main(int argc, const char* argv[]) {
|
||||
coreParameter.mountRoot = mountRoot.empty() ? Path() : Path(mountRoot);
|
||||
coreParameter.startBreak = false;
|
||||
coreParameter.headLess = true;
|
||||
coreParameter.dumpCompat = cmdLineOptions.dumpCompat.value_or(false);
|
||||
coreParameter.loadGameConfigs = false;
|
||||
coreParameter.renderScaleFactor = cmdLineOptions.resolutionScale.value_or(1);
|
||||
coreParameter.renderWidth = 480 * coreParameter.renderScaleFactor;
|
||||
|
||||
Reference in new issue
Block a user