From da028244f6f95fe89bf77f61e47815869ac8b973 Mon Sep 17 00:00:00 2001 From: Hinrik Gylfason Date: Tue, 27 Oct 2026 19:16:53 +0000 Subject: [PATCH 1/2] Add Tr2Sprite2dEffect, a sprite that draws an effect over its rect A sprite's look is fixed by the uber shader, so drawing a custom shader over one UI element has so far taken a Tr2Sprite2dRenderJob per element. Each one runs its job outside the scene's batch: the scene flushes what it has drawn, leaves managed rendering, swaps the depth stencil, glow target and viewport around the job, and restores its render states and vertex declaration after it. The job knows nothing of where its sprite is on screen or how opaque its ancestors are, so the caller has to work that out every frame and place a viewport. A viewport is axis-aligned and the glow target is unbound inside a job, so the effect neither rotates with its ancestors nor writes glow. Tr2Sprite2dEffect is a leaf sprite that draws one effect over its quad, in order with the other sprites. The quad goes through the scene's batched path like any sprite's, so it is clipped, transformed, captured into display lists and drawn in 3D as they are, with no per-frame work outside the scene. The effect takes the place of the uber shader for that one draw call, with the uber shader's vertex layout and constants. The sprite fills the vertex fields for its own purpose: texcoord 0 runs 0 to 1 over the quad, texcoord 1 holds the quad's width and height, and the sprite's color, glow brightness and sprite target ride in the vertices. Each effect sprite is a draw call of its own, which breaks the scene's batch, but that is the whole of its cost. The sprite builds its quad itself and sets every vertex field, so the quad doesn't depend on what earlier sprites drew. A new Tr2Sprite2dScene::RenderTriangleVerts overload draws the vertices as a draw call of their own, with the given effect overriding the scene's shader while it does. An override effect gets none of the scene's textures, as the scene binds them after the effect binds its own, so neither the scene nor a display list entry captured from it binds them. --- trinity/CMakeLists.txt | 3 + trinity/Sprite2d/Tr2Sprite2dDisplayList.cpp | 7 +- trinity/Sprite2d/Tr2Sprite2dDisplayList.h | 2 + trinity/Sprite2d/Tr2Sprite2dEffect.cpp | 87 +++++++++++++++++++++ trinity/Sprite2d/Tr2Sprite2dEffect.h | 47 +++++++++++ trinity/Sprite2d/Tr2Sprite2dEffect_Blue.cpp | 41 ++++++++++ trinity/Sprite2d/Tr2Sprite2dScene.cpp | 44 ++++++++--- trinity/Sprite2d/Tr2Sprite2dScene.h | 13 +++ 8 files changed, 233 insertions(+), 11 deletions(-) create mode 100644 trinity/Sprite2d/Tr2Sprite2dEffect.cpp create mode 100644 trinity/Sprite2d/Tr2Sprite2dEffect.h create mode 100644 trinity/Sprite2d/Tr2Sprite2dEffect_Blue.cpp diff --git a/trinity/CMakeLists.txt b/trinity/CMakeLists.txt index cbf221aef..1c92eedee 100644 --- a/trinity/CMakeLists.txt +++ b/trinity/CMakeLists.txt @@ -1346,6 +1346,9 @@ set(_SOURCES Sprite2d/Tr2Sprite2dContainer_Blue.cpp Sprite2d/Tr2Sprite2dDisplayList.cpp Sprite2d/Tr2Sprite2dDisplayList.h + Sprite2d/Tr2Sprite2dEffect.cpp + Sprite2d/Tr2Sprite2dEffect.h + Sprite2d/Tr2Sprite2dEffect_Blue.cpp Sprite2d/Tr2Sprite2dFrame.cpp Sprite2d/Tr2Sprite2dFrame.h Sprite2d/Tr2Sprite2dFrame_Blue.cpp diff --git a/trinity/Sprite2d/Tr2Sprite2dDisplayList.cpp b/trinity/Sprite2d/Tr2Sprite2dDisplayList.cpp index 013971fd6..5cb5d1346 100644 --- a/trinity/Sprite2d/Tr2Sprite2dDisplayList.cpp +++ b/trinity/Sprite2d/Tr2Sprite2dDisplayList.cpp @@ -66,8 +66,11 @@ void Tr2Sprite2dDisplayList::Entry::SubmitGeometry( Tr2RenderContext& renderCont transposedMatrixes[i] = Transpose( transformArray[i] ); } - renderContext.SetSrv( PIXEL_SHADER, textureRegisters[0], ( texture0 && texture0->GetTexture() ) ? *texture0->GetTexture() : Tr2TextureAL(), colorSpace ); - renderContext.SetSrv( PIXEL_SHADER, textureRegisters[1], ( texture1 && texture1->GetTexture() ) ? *texture1->GetTexture() : Tr2TextureAL(), colorSpace ); + if( bindTextures ) + { + renderContext.SetSrv( PIXEL_SHADER, textureRegisters[0], ( texture0 && texture0->GetTexture() ) ? *texture0->GetTexture() : Tr2TextureAL(), colorSpace ); + renderContext.SetSrv( PIXEL_SHADER, textureRegisters[1], ( texture1 && texture1->GetTexture() ) ? *texture1->GetTexture() : Tr2TextureAL(), colorSpace ); + } FillAndSetConstants( *m_uiTransformsCb, diff --git a/trinity/Sprite2d/Tr2Sprite2dDisplayList.h b/trinity/Sprite2d/Tr2Sprite2dDisplayList.h index 6e08e587b..37cbe00cf 100644 --- a/trinity/Sprite2d/Tr2Sprite2dDisplayList.h +++ b/trinity/Sprite2d/Tr2Sprite2dDisplayList.h @@ -33,6 +33,8 @@ struct Tr2Sprite2dDisplayList : public Tr2DeviceResource Tr2AtlasTexturePtr texture0; Tr2AtlasTexturePtr texture1; uint32_t textureRegisters[2]; + // Whether to bind texture0 and texture1. False only for an override effect. + bool bindTextures; Tr2RenderContextEnum::ColorSpace colorSpace; Tr2EffectPtr effect; //hardcoded due to need for CB slot//Tr2EffectConstant transformsHandle; diff --git a/trinity/Sprite2d/Tr2Sprite2dEffect.cpp b/trinity/Sprite2d/Tr2Sprite2dEffect.cpp new file mode 100644 index 000000000..d9e97d504 --- /dev/null +++ b/trinity/Sprite2d/Tr2Sprite2dEffect.cpp @@ -0,0 +1,87 @@ +// Copyright © 2026 CCP ehf. + +#include "StdAfx.h" +#include "Tr2Sprite2dEffect.h" +#include "Tr2Sprite2dScene.h" +#include "Tr2Sprite2dPickingMask.h" +#include "Shader/Tr2Effect.h" + +static unsigned short s_quadIndices[6] = { 0, 1, 3, 3, 1, 2 }; + +Tr2Sprite2dEffect::Tr2Sprite2dEffect( IRoot* lockobj /*= NULL */ ) : + m_color( 1.0f, 1.0f, 1.0f, 1.0f ), + m_glowBrightness( 1.0f ), + m_target( Tr2SpriteTarget::COLOR ) +{ +} + +Tr2Sprite2dEffect::~Tr2Sprite2dEffect() +{ +} + +void Tr2Sprite2dEffect::GatherSprites( Tr2Sprite2dScene* renderer ) +{ + TRINITY_STATS_ZONE( __FUNCTION__ ); + + if( !m_display || !m_effect ) + { + return; + } + + static const Vector2 s_corners[4] = { + Vector2( 0.0f, 0.0f ), + Vector2( 1.0f, 0.0f ), + Vector2( 1.0f, 1.0f ), + Vector2( 0.0f, 1.0f ) + }; + + // Every field is set here, from the sprite or as a default Tr2SpriteObject + // would set it, so the effect gets the same vertices whatever the sprites + // before it left + renderer->SetDepth( 0.0f ); + + Tr2Sprite2dD3DVertex vertices[4]; + for( int i = 0; i < 4; ++i ) + { + Tr2Sprite2dD3DVertex& vertex = vertices[i]; + vertex.position = Vector3( m_translation.x + s_corners[i].x * m_displayWidth, m_translation.y + s_corners[i].y * m_displayHeight, 0.0f ); + vertex.color = m_color; + vertex.texCoord[0] = s_corners[i]; + vertex.texCoord[1] = Vector2( m_displayWidth, m_displayHeight ); + vertex.glowBrightness = m_glowBrightness; + vertex.transformIndex = 0; + vertex.blendMode = PackBlendMode( TR2_SBM_NONE, m_target ); + vertex.spriteEffect = TR2_SFX_FILL; + vertex.tileMode = S2D_TS_NONE; + vertex.outlineColor = Color( 0.0f, 0.0f, 0.0f, 1.0f ); + vertex.outlineThreshold = 0.0f; + } + + renderer->RenderTriangleVerts( *m_effect, vertices, 4, s_quadIndices, 6 ); +} + +ITr2SpriteObject* Tr2Sprite2dEffect::PickPoint( float x, float y, Tr2Sprite2dScene* renderer ) +{ + if( !m_display ) + { + return NULL; + } + + if( m_pickState == TR2_SPS_ON ) + { + if( renderer->IsInside( Vector2( x, y ), m_translation, m_displayWidth, m_displayHeight, 0.0f ) ) + { + if( !m_pickingMask || m_pickingMask->SampleMask( renderer->InverseTransformPoint( Vector2( x, y ) ), m_translation, m_displayWidth, m_displayHeight ) ) + { + return this; + } + } + } + + return NULL; +} + +unsigned int Tr2Sprite2dEffect::GetVertexCount() +{ + return ( m_display && m_effect ) ? 4 : 0; +} diff --git a/trinity/Sprite2d/Tr2Sprite2dEffect.h b/trinity/Sprite2d/Tr2Sprite2dEffect.h new file mode 100644 index 000000000..48ad46b23 --- /dev/null +++ b/trinity/Sprite2d/Tr2Sprite2dEffect.h @@ -0,0 +1,47 @@ +// Copyright © 2026 CCP ehf. + +#pragma once +#ifndef Tr2Sprite2dEffect_h +#define Tr2Sprite2dEffect_h + +#include "ITr2SpriteObject.h" +#include "Tr2SpriteObject.h" + +BLUE_DECLARE( Tr2Effect ); +BLUE_DECLARE( Tr2Sprite2dEffect ); + +// A leaf sprite that draws one effect over its rect, in order with the other +// sprites, as a draw call of its own. The effect draws the sprite's quad in +// place of the scene's own shaders, so it gets the same vertices and +// constants (UIVertexInput, g_uiTransforms and UIViewportSize in +// Carbon/UI/UI.fxh), clips and transforms them as they do, and should write +// glow to SV_Target1 like them. Texcoord 0 runs 0 to 1 over the quad, texcoord +// 1 is the quad's width and height, and the vertex color is the sprite's color +// with its ancestors' opacity multiplied into alpha. The glow brightness and +// sprite target are the sprite's own, and the other vertex fields hold a +// default sprite's values, whatever was drawn before. The effect gets none of +// the scene's textures. +class Tr2Sprite2dEffect : public Tr2SpriteObjectBase +{ +public: + EXPOSE_TO_BLUE(); + + Tr2Sprite2dEffect( IRoot* lockobj = NULL ); + ~Tr2Sprite2dEffect(); + + ////////////////////////////////////////////////////////////////////////// + // ITr2SpriteObject + virtual unsigned int GetVertexCount(); + virtual void GatherSprites( Tr2Sprite2dScene* renderer ); + virtual ITr2SpriteObject* PickPoint( float x, float y, Tr2Sprite2dScene* renderer ); + +private: + Tr2EffectPtr m_effect; + Color m_color; + float m_glowBrightness; + Tr2SpriteTarget m_target; +}; + +TYPEDEF_BLUECLASS( Tr2Sprite2dEffect ); + +#endif diff --git a/trinity/Sprite2d/Tr2Sprite2dEffect_Blue.cpp b/trinity/Sprite2d/Tr2Sprite2dEffect_Blue.cpp new file mode 100644 index 000000000..01bf5b1b2 --- /dev/null +++ b/trinity/Sprite2d/Tr2Sprite2dEffect_Blue.cpp @@ -0,0 +1,41 @@ +// Copyright © 2026 CCP ehf. + +#include "StdAfx.h" +#include "Tr2Sprite2dEffect.h" + +BLUE_DEFINE( Tr2Sprite2dEffect ); + +const Be::ClassInfo* Tr2Sprite2dEffect::ExposeToBlue() +{ + EXPOSURE_BEGIN( Tr2Sprite2dEffect, "" ) + MAP_INTERFACE( ITr2SpriteObject ) + MAP_INTERFACE( Tr2Sprite2dEffect ) + + MAP_ATTRIBUTE( + "effect", + m_effect, + "The effect drawn over the sprite's quad in place of the scene's shaders, or None. It gets the\n" + "scene's vertices and constants (see Carbon/UI/UI.fxh), with texcoord 0 running 0 to 1 over the quad,\n" + "texcoord 1 holding the quad's width and height, and the sprite's color in the vertex color.", + Be::READWRITE | Be::NOTIFY ) + + MAP_ATTRIBUTE( + "color", + m_color, + "Color of the sprite, passed to the effect as the vertex color. Its alpha is multiplied with its ancestors' opacity.", + Be::READWRITE | Be::NOTIFY ) + + MAP_ATTRIBUTE( + "glowBrightness", + m_glowBrightness, + "Glow brightness passed to the effect in the vertices. See spriteTarget.", + Be::READWRITE | Be::NOTIFY ) + + MAP_ATTRIBUTE_WITH_CHOOSER( + "spriteTarget", + m_target, + "Render target passed to the effect in the vertices: either color or glow target", + Be::READWRITE | Be::ENUM | Be::NOTIFY, + Tr2SpriteTargetChooser ) + EXPOSURE_CHAINTO( Tr2SpriteObjectBase ) +} diff --git a/trinity/Sprite2d/Tr2Sprite2dScene.cpp b/trinity/Sprite2d/Tr2Sprite2dScene.cpp index f914ab0a6..dc3fc4953 100644 --- a/trinity/Sprite2d/Tr2Sprite2dScene.cpp +++ b/trinity/Sprite2d/Tr2Sprite2dScene.cpp @@ -117,6 +117,7 @@ Tr2Sprite2dScene::Tr2Sprite2dScene( IRoot* lockobj ) : m_maxSpriteCount( 1024 ), m_tileMode( S2D_TS_NONE ), m_drawCallStartIndex( 0 ), + m_effectOverride( nullptr ), m_transformsHandle(), m_useLinearColorSpace( false ), m_isGammaCorrectingText( true ), @@ -1041,6 +1042,17 @@ void Tr2Sprite2dScene::RenderTriangleVerts( Tr2Sprite2dD3DVertex* verticesSrc, u CopyIndicesWithOffset( indices, indexCount, vertexOffset ); } +void Tr2Sprite2dScene::RenderTriangleVerts( Tr2Effect& effect, Tr2Sprite2dD3DVertex* verticesSrc, unsigned int vertexCount, unsigned short* indices, unsigned int indexCount ) +{ + // Draw what's already batched with the scene's shader, then batch the + // vertices on their own and draw them with the effect + IssueDrawCall(); + m_effectOverride = &effect; + RenderTriangleVerts( verticesSrc, vertexCount, indices, indexCount ); + IssueDrawCall(); + m_effectOverride = nullptr; +} + void Tr2Sprite2dScene::RenderTriangleVerts( Tr2BufferAL& verticesSrc, unsigned int vertexCount, Tr2BufferAL& indices, unsigned int indexCount ) { CCP_ASSERT( !m_captureDisplayList ); @@ -1103,6 +1115,7 @@ void Tr2Sprite2dScene::IssueDrawCall() entry.texture0 = m_texture[0]; entry.texture1 = m_texture[1]; entry.effect = m_effect; + entry.bindTextures = !m_effectOverride; m_texelSizeVar[0].GetValue( entry.texelSize0 ); m_texelSizeVar[1].GetValue( entry.texelSize1 ); @@ -1177,22 +1190,26 @@ void Tr2Sprite2dScene::SubmitGeometry( Tr2RenderContext& renderContext ) transposedMatrixes[i] = Transpose( m_transformArray[i] ); } - for( uint32_t i = 0; i < 2; ++i ) + // An override effect gets none of the scene's textures + if( !m_effectOverride ) { - Tr2TextureAL* texAL = nullptr; - if( m_texture[i] ) + for( uint32_t i = 0; i < 2; ++i ) { - texAL = m_texture[i]->GetTexture(); - if( !texAL ) + Tr2TextureAL* texAL = nullptr; + if( m_texture[i] ) { - if( m_texture[i]->GetRenderTarget() ) + texAL = m_texture[i]->GetTexture(); + if( !texAL ) { - texAL = m_texture[i]->GetRenderTarget(); + if( m_texture[i]->GetRenderTarget() ) + { + texAL = m_texture[i]->GetRenderTarget(); + } } } + auto colorSpace = m_useLinearColorSpace ? Tr2RenderContextEnum::COLOR_SPACE_SRGB : Tr2RenderContextEnum::COLOR_SPACE_LINEAR; + renderContext.SetSrv( PIXEL_SHADER, m_textureRegisters[i], texAL ? *texAL : Tr2TextureAL(), colorSpace ); } - auto colorSpace = m_useLinearColorSpace ? Tr2RenderContextEnum::COLOR_SPACE_SRGB : Tr2RenderContextEnum::COLOR_SPACE_LINEAR; - renderContext.SetSrv( PIXEL_SHADER, m_textureRegisters[i], texAL ? *texAL : Tr2TextureAL(), colorSpace ); } bool result = FillAndSetConstants( @@ -1718,8 +1735,17 @@ void Tr2Sprite2dScene::SetOutlineThreshold( float outlineThreshold ) m_outlineThreshold = outlineThreshold; } +// Selects the effect for the batched vertices and returns whether it can draw +// them. An override effect always can, even before its shader loads, so that +// a display list captured meanwhile still records its draw call. bool Tr2Sprite2dScene::SelectEffect() { + if( m_effectOverride ) + { + m_effect = m_effectOverride; + return true; + } + Tr2Effect* newEffect; newEffect = m_is2dRender ? m_uberShader2d : m_uberShader3d; diff --git a/trinity/Sprite2d/Tr2Sprite2dScene.h b/trinity/Sprite2d/Tr2Sprite2dScene.h index d68e0ac98..0f8f08f07 100644 --- a/trinity/Sprite2d/Tr2Sprite2dScene.h +++ b/trinity/Sprite2d/Tr2Sprite2dScene.h @@ -117,6 +117,15 @@ class Tr2Sprite2dScene : public ITr2Scene, unsigned short* indices, unsigned int indexCount ); + // Draws the vertices as a draw call of their own, with the given effect in + // place of the scene's shader and none of the scene's textures. + void RenderTriangleVerts( + Tr2Effect& effect, + Tr2Sprite2dD3DVertex* verticesSrc, + unsigned int vertexCount, + unsigned short* indices, + unsigned int indexCount ); + void RenderTriangleVerts( Tr2BufferAL& verticesSrc, unsigned int vertexCount, @@ -202,6 +211,10 @@ class Tr2Sprite2dScene : public ITr2Scene, Tr2EffectPtr m_effect; uint32_t m_textureRegisters[2]; + // Effect to draw with instead of the scene's shader. Only set inside + // RenderTriangleVerts with an effect. + Tr2Effect* m_effectOverride; + Tr2EffectConstant m_transformsHandle; // The desired visual effect From ad25ee5a7ca38f45d443a50fcecd825b61b2080f Mon Sep 17 00:00:00 2001 From: Hinrik Gylfason Date: Wed, 28 Oct 2026 15:15:57 +0000 Subject: [PATCH 2/2] Address review comments on Tr2Sprite2dEffect Initialize bindTextures on the display list entry that RunJob captures too, so every Entry sets it. Mark the ITr2SpriteObject overrides with override instead of virtual, and drop the empty destructor. GetVertexCount stays a plain method, as on every other sprite: no base class declares it virtual. --- trinity/Sprite2d/Tr2Sprite2dEffect.cpp | 4 ---- trinity/Sprite2d/Tr2Sprite2dEffect.h | 7 +++---- trinity/Sprite2d/Tr2Sprite2dScene.cpp | 1 + 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/trinity/Sprite2d/Tr2Sprite2dEffect.cpp b/trinity/Sprite2d/Tr2Sprite2dEffect.cpp index d9e97d504..37015b372 100644 --- a/trinity/Sprite2d/Tr2Sprite2dEffect.cpp +++ b/trinity/Sprite2d/Tr2Sprite2dEffect.cpp @@ -15,10 +15,6 @@ Tr2Sprite2dEffect::Tr2Sprite2dEffect( IRoot* lockobj /*= NULL */ ) : { } -Tr2Sprite2dEffect::~Tr2Sprite2dEffect() -{ -} - void Tr2Sprite2dEffect::GatherSprites( Tr2Sprite2dScene* renderer ) { TRINITY_STATS_ZONE( __FUNCTION__ ); diff --git a/trinity/Sprite2d/Tr2Sprite2dEffect.h b/trinity/Sprite2d/Tr2Sprite2dEffect.h index 48ad46b23..85608fdd2 100644 --- a/trinity/Sprite2d/Tr2Sprite2dEffect.h +++ b/trinity/Sprite2d/Tr2Sprite2dEffect.h @@ -27,13 +27,12 @@ class Tr2Sprite2dEffect : public Tr2SpriteObjectBase EXPOSE_TO_BLUE(); Tr2Sprite2dEffect( IRoot* lockobj = NULL ); - ~Tr2Sprite2dEffect(); ////////////////////////////////////////////////////////////////////////// // ITr2SpriteObject - virtual unsigned int GetVertexCount(); - virtual void GatherSprites( Tr2Sprite2dScene* renderer ); - virtual ITr2SpriteObject* PickPoint( float x, float y, Tr2Sprite2dScene* renderer ); + unsigned int GetVertexCount(); + void GatherSprites( Tr2Sprite2dScene* renderer ) override; + ITr2SpriteObject* PickPoint( float x, float y, Tr2Sprite2dScene* renderer ) override; private: Tr2EffectPtr m_effect; diff --git a/trinity/Sprite2d/Tr2Sprite2dScene.cpp b/trinity/Sprite2d/Tr2Sprite2dScene.cpp index dc3fc4953..7ba4b4dd8 100644 --- a/trinity/Sprite2d/Tr2Sprite2dScene.cpp +++ b/trinity/Sprite2d/Tr2Sprite2dScene.cpp @@ -1817,6 +1817,7 @@ void Tr2Sprite2dScene::RunJob( TriRenderJob* job ) entry.primitiveCount = 0; entry.texture0 = nullptr; entry.texture1 = nullptr; + entry.bindTextures = true; entry.texelSize0 = Vector4( 0, 0, 0, 0 ); entry.texelSize1 = Vector4( 0, 0, 0, 0 );