Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Apps/UnitTests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ set(SOURCES
"Source/Tests.NativeEngine.Teardown.cpp"
"Source/Tests.ShaderCache.cpp"
"Source/Tests.ShaderCompilation.cpp"
"Source/Tests.ShaderCompilation.FragCoord.cpp"
"Source/Tests.UniformPadding.cpp"
"Source/Helpers.h"
"Source/Helpers.${GRAPHICS_API}.${BABYLON_NATIVE_PLATFORM_IMPL_EXT}")
Expand Down
437 changes: 437 additions & 0 deletions Apps/UnitTests/Source/Tests.ShaderCompilation.FragCoord.cpp

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ namespace Babylon::Graphics
}
return false;
}
/// Uniform declared in fragment shaders that read gl_FragCoord, holding the bound
/// framebuffer's width/height in .x/.y. Deliberately outside the u_ namespace Babylon.js
/// uses so it cannot collide with a shader uniform.
inline constexpr const char* FRAGCOORD_TARGET_SIZE_UNIFORM_NAME{"bnFragCoordTargetSize"};

struct BgfxShaderInfo
{
Expand Down
14 changes: 14 additions & 0 deletions Plugins/NativeEngine/Source/NativeEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3066,6 +3066,20 @@ namespace Babylon
encoder->setUniform({it.first}, value.Data.data(), value.ElementLength);
}

// Resolves the gl_FragCoord Y flip injected by ShaderCompilerTraversers::FlipFragCoordY.
// Must be the framebuffer's height, not the bgfx view rect's, which
// SetBgfxViewPortAndScissor narrows to the viewport when one is set.
if (const UniformInfo* fragCoordTargetSize = m_currentProgram->FragCoordTargetSizeUniform())
{
const Graphics::FrameBuffer& frameBuffer = GetBoundFrameBuffer();
const float targetSize[4]{
static_cast<float>(frameBuffer.Width()),
static_cast<float>(frameBuffer.Height()),
0.0f,
0.0f};
encoder->setUniform(fragCoordTargetSize->Handle, targetSize, 1);
}

// Generic divisor-driven attributes are per-vertex inputs in the base shader. Recompile a
// variant that routes each one to the exact i_data slot used by the instance buffer.
// Built-ins keep the compiler-assigned base slots carried by Program.
Expand Down
3 changes: 3 additions & 0 deletions Plugins/NativeEngine/Source/Program.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ namespace Babylon
throw std::runtime_error{"Multiple built-in instance attributes use the same shader location."};
}
}

// Cached because DrawInternal consults it on every draw.
m_fragCoordTargetSizeUniform = GetUniformInfo(Graphics::FRAGCOORD_TARGET_SIZE_UNIFORM_NAME);
}

void Program::SetSources(std::string vertexSource, std::string fragmentSource)
Expand Down
3 changes: 3 additions & 0 deletions Plugins/NativeEngine/Source/Program.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ namespace Babylon
bgfx::ProgramHandle Handle() const { return m_handle; }
const std::map<uint16_t, UniformValue>& Uniforms() const { return m_uniforms; }
const std::map<std::string, uint32_t>& VertexAttributeLocations() const { return m_vertexAttributeLocations; }
// Null for shaders that never read gl_FragCoord; the compiler omits the uniform there.
const UniformInfo* FragCoordTargetSizeUniform() const { return m_fragCoordTargetSizeUniform; }

// Compiler-assigned i_data slot for each built-in attribute location.
const std::map<uint32_t, uint32_t>& BuiltInInstanceDataSlots() const { return m_builtInInstanceDataSlots; }
Expand All @@ -82,6 +84,7 @@ namespace Babylon
std::map<uint16_t, UniformInfo> m_uniformInfos;
std::map<std::string, uint32_t> m_vertexAttributeLocations;
std::map<uint32_t, uint32_t> m_builtInInstanceDataSlots;
const UniformInfo* m_fragCoordTargetSizeUniform{nullptr};
std::string m_vertexSource;
std::string m_fragmentSource;
std::map<std::map<std::string, uint32_t>, bgfx::ProgramHandle> m_instancedVariants;
Expand Down
4 changes: 3 additions & 1 deletion Plugins/ShaderCache/Source/ShaderCacheImpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ namespace Babylon::Plugins::ShaderCache
// 4: store the compiler-assigned built-in instance-data slots.
// 5: shader binary packaging bumped to bgfx BGFX_SHADER_BIN_VERSION 12 (raw
// SRV/UAV masks + tex meta); stale v4 cache entries would be rejected by bgfx.
static const uint32_t CACHE_VERSION = 5;
// 6: FlipFragCoordY rewrites gl_FragCoord and injects bnFragCoordTargetSize;
// version-5 entries for those shaders skip compilation and omit the uniform.
static const uint32_t CACHE_VERSION = 6;

void ShaderCacheImpl::Clear()
{
Expand Down
2 changes: 2 additions & 0 deletions Plugins/ShaderCompiler/Source/ShaderCompilerDXBC.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ namespace Babylon::Plugins
ShaderCompilerTraversers::IdGenerator ids{};
// Flip 2D texture sample coordinates (replaces the former ProcessSamplerFlip texture() macro).
ShaderCompilerTraversers::FlipSamplerCoordinates(program);
// Must precede the uniform struct move, which collects the uniform this declares.
ShaderCompilerTraversers::FlipFragCoordY(program, ids);
Comment thread
bghgary marked this conversation as resolved.
auto cutScope = ShaderCompilerTraversers::ChangeUniformTypes(program, ids);
auto utstScope = ShaderCompilerTraversers::MoveNonSamplerUniformsIntoStruct(program, ids);
std::map<std::string, std::string> vertexAttributeRenaming = {};
Expand Down
2 changes: 2 additions & 0 deletions Plugins/ShaderCompiler/Source/ShaderCompilerDXIL.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ namespace Babylon::Plugins
ShaderCompilerTraversers::IdGenerator ids{};
// Flip 2D texture sample coordinates (replaces the former ProcessSamplerFlip texture() macro).
ShaderCompilerTraversers::FlipSamplerCoordinates(program);
// Must precede the uniform struct move, which collects the uniform this declares.
ShaderCompilerTraversers::FlipFragCoordY(program, ids);
auto cutScope = ShaderCompilerTraversers::ChangeUniformTypes(program, ids);
auto utstScope = ShaderCompilerTraversers::MoveNonSamplerUniformsIntoStruct(program, ids);
std::map<std::string, std::string> vertexAttributeRenaming = {};
Expand Down
2 changes: 2 additions & 0 deletions Plugins/ShaderCompiler/Source/ShaderCompilerMetal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ namespace Babylon::Plugins
ShaderCompilerTraversers::IdGenerator ids{};
// Flip 2D texture sample coordinates (replaces the former ProcessSamplerFlip texture() macro).
ShaderCompilerTraversers::FlipSamplerCoordinates(program);
// Must precede the uniform struct move, which collects the uniform this declares.
ShaderCompilerTraversers::FlipFragCoordY(program, ids);
auto cutScope = ShaderCompilerTraversers::ChangeUniformTypes(program, ids);
auto utstScope = ShaderCompilerTraversers::MoveNonSamplerUniformsIntoStruct(program, ids);
std::map<std::string, std::string> vertexAttributeRenaming = {};
Expand Down
151 changes: 151 additions & 0 deletions Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ namespace Babylon::ShaderCompilerTraversers
branch->setFalseBlock(replacement);
}
}
else if (auto* flow = parent->getAsBranchNode())
{
// `return gl_FragCoord;` (and similar) parents the symbol on TIntermBranch.
if (flow->getExpression() != symbol)
{
throw std::runtime_error{"Cannot replace symbol: unexpected branch expression"};
}
RemoveAllTreeNodes(flow->getExpression());
flow->setExpression(replacement);
}
else
{
throw std::runtime_error{"Cannot replace symbol: node type handler unimplemented"};
Expand Down Expand Up @@ -1422,6 +1432,15 @@ namespace Babylon::ShaderCompilerTraversers
selection->setFalseBlock(replacement);
}
}
else if (auto* flow = parent->getAsBranchNode())
{
if (flow->getExpression() != oldSymbol)
{
throw std::runtime_error{
"SamplerFunctionParameterSplitter: unexpected branch expression when rewriting body sampler reference"};
}
flow->setExpression(replacement);
}
else
{
throw std::runtime_error{
Expand Down Expand Up @@ -2354,6 +2373,133 @@ namespace Babylon::ShaderCompilerTraversers

TIntermediate* m_intermediate{};
};

/// Presents gl_FragCoord in OpenGL's coordinate space on the top-left-origin backends
/// (D3D, Metal, Vulkan). FlipSamplerCoordinates already flips every sample coordinate, so
/// gl_FragCoord was the one input left in physical space -- making
/// `texelFetch(tex, ivec2(gl_FragCoord.xy), 0)` read the mirrored row.
///
/// The flip is `targetHeight - gl_FragCoord.y`, with no -1 term: the hardware yields
/// p + 0.5 for physical row p, and p == height - 1 - y, so the GL value y + 0.5 is exactly
/// height minus the incoming value.
///
/// The height cannot come from bgfx's u_viewRect, which SetBgfxViewPortAndScissor narrows
/// to the viewport, while gl_FragCoord is relative to the whole render target.
class FragCoordYFlipTraverser final : private TIntermTraverser
{
public:
static void Traverse(TProgram& program, IdGenerator& ids)
{
auto* intermediate{program.getIntermediate(EShLangFragment)};
if (intermediate == nullptr)
{
return;
}

FragCoordYFlipTraverser traverser{intermediate};
intermediate->getTreeRoot()->traverse(&traverser);

if (traverser.m_symbolsToParents.empty())
{
return;
}

// Declared as a linker object so MoveNonSamplerUniformsIntoStruct sweeps it into
// the "Frame" struct with every other non-sampler uniform.
TType targetSizeType{EbtFloat, EvqUniform, 4};
TIntermSymbol* targetSize{intermediate->addSymbol(TIntermSymbol{ids.Next(), Graphics::FRAGCOORD_TARGET_SIZE_UNIFORM_NAME, targetSizeType})};

auto* linkerObjects = FindLinkerObjects(intermediate->getTreeRoot()->getAsAggregate());
if (linkerObjects == nullptr)
{
throw std::runtime_error{"FragCoordYFlip: fragment stage has no linker objects sequence."};
}
linkerObjects->getSequence().push_back(targetSize);

traverser.ApplyReplacements(targetSize);
}

protected:
void visitSymbol(TIntermSymbol* symbol) override
{
// Linker object references declare gl_FragCoord rather than read it.
if (symbol->getName() != "gl_FragCoord" || IsLinkerObject(path))
{
return;
}

m_symbolsToParents.emplace_back(symbol, getParentNode());
}

private:
FragCoordYFlipTraverser(TIntermediate* intermediate)
: TIntermTraverser{true, false, false}
, m_intermediate{intermediate}
{
}

static TIntermAggregate* FindLinkerObjects(TIntermAggregate* root)
{
if (root == nullptr)
{
return nullptr;
}

for (auto* node : root->getSequence())
{
auto* aggregate = node != nullptr ? node->getAsAggregate() : nullptr;
if (aggregate != nullptr && aggregate->getOp() == EOpLinkerObjects)
{
return aggregate;
}
}

return nullptr;
}

void ApplyReplacements(TIntermSymbol* targetSize)
{
for (const auto& [symbol, parent] : m_symbolsToParents)
{
// Not batched into one MakeReplacements call: that maps one replacement per
// symbol *name*, so every gl_FragCoord reference would share one subtree and
// that node would end up with multiple parents.
MakeReplacements({{"gl_FragCoord", BuildFlippedFragCoord(symbol, targetSize)}}, {{symbol, parent}});
Comment thread
bghgary marked this conversation as resolved.
}
}

/// Builds `vec4(gl_FragCoord.x, targetSize.y - gl_FragCoord.y, .z, .w)`. The whole
/// vector is rebuilt rather than patching .y because a reference may be swizzled,
/// indexed, or passed along whole, and the parent node is not inspected here.
TIntermTyped* BuildFlippedFragCoord(TIntermSymbol* fragCoord, TIntermSymbol* targetSize)
{
const TSourceLoc& loc{fragCoord->getLoc()};
TType floatType{EbtFloat, EvqTemporary, 1};
TType vec4Type{EbtFloat, EvqTemporary, 4};

// Each component gets its own symbol copy so no node ends up with two parents.
auto component = [&](int index) {
TIntermTyped* copy{m_intermediate->addSymbol(*fragCoord)};
TIntermTyped* element{m_intermediate->addIndex(EOpIndexDirect, copy, m_intermediate->addConstantUnion(index, loc), loc)};
element->setType(floatType);
return element;
};

TIntermTyped* height{m_intermediate->addIndex(EOpIndexDirect, m_intermediate->addSymbol(*targetSize), m_intermediate->addConstantUnion(1, loc), loc)};
height->setType(floatType);

TIntermTyped* flippedY{m_intermediate->addBinaryMath(EOpSub, height, component(1), loc)};

TIntermAggregate* constructed{m_intermediate->makeAggregate(component(0), loc)};
constructed = m_intermediate->growAggregate(constructed, flippedY, loc);
constructed = m_intermediate->growAggregate(constructed, component(2), loc);
constructed = m_intermediate->growAggregate(constructed, component(3), loc);
return m_intermediate->setAggregateOperator(constructed, EOpConstructVec4, vec4Type, loc);
}

TIntermediate* m_intermediate{};
std::vector<std::pair<TIntermSymbol*, TIntermNode*>> m_symbolsToParents{};
};
}

ScopeT MoveNonSamplerUniformsIntoStruct(TProgram& program, IdGenerator& ids)
Expand Down Expand Up @@ -2410,4 +2556,9 @@ namespace Babylon::ShaderCompilerTraversers
{
FlipSamplerCoordinatesTraverser::Traverse(program);
}

void FlipFragCoordY(TProgram& program, IdGenerator& ids)
{
FragCoordYFlipTraverser::Traverse(program, ids);
}
}
6 changes: 6 additions & 0 deletions Plugins/ShaderCompiler/Source/ShaderCompilerTraversers.h
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,10 @@ namespace Babylon::ShaderCompilerTraversers
/// Must only be used on the backends that apply ProcessSamplerFlip (D3D, Metal, Vulkan); the
/// OpenGL backend shares bgfx's V-orientation and must not flip.
void FlipSamplerCoordinates(glslang::TProgram& program);

/// Rewrite every read of gl_FragCoord to present it in OpenGL's bottom-left-origin space.
/// Must run before MoveNonSamplerUniformsIntoStruct so the target-size uniform it declares is
/// collected with the others, and only on the backends that apply FlipSamplerCoordinates
/// (D3D, Metal, Vulkan); OpenGL already matches WebGL's origin.
void FlipFragCoordY(glslang::TProgram& program, IdGenerator& ids);
}
2 changes: 2 additions & 0 deletions Plugins/ShaderCompiler/Source/ShaderCompilerVulkan.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,8 @@ namespace Babylon::Plugins
ShaderCompilerTraversers::IdGenerator ids{};
// Flip 2D texture sample coordinates (replaces the former ProcessSamplerFlip texture() macro).
ShaderCompilerTraversers::FlipSamplerCoordinates(program);
// Must precede the uniform struct move, which collects the uniform this declares.
ShaderCompilerTraversers::FlipFragCoordY(program, ids);
auto cutScope = ShaderCompilerTraversers::ChangeUniformTypes(program, ids);
auto utstScope = ShaderCompilerTraversers::MoveNonSamplerUniformsIntoStruct(program, ids);
std::map<std::string, std::string> vertexAttributeRenaming = {};
Expand Down