-
Notifications
You must be signed in to change notification settings - Fork 6
en RenderHandle Pipeline
Documentation moved: This page is archived. See the up-to-date guide at innovault.wiki.
Client render pipeline: maps Terraria draw stages to overridable virtual methods; manages
RenderTarget2Dand instance ordering.
Client / GPU only: On dedicated server (Main.dedServ), ScreenTargets are not created and UpdateBySystem plus all draw hooks are skipped.
RenderHandle is the abstract base class (VaultType<RenderHandle>) for InnoVault's render pipeline. RenderHandleLoader maps Terraria hooks (On_Main.*, On_FilterManager.EndCapture, etc.) to layered virtual methods; each registered instance is invoked in ascending Weight order.
Good for:
- Full-screen post-process, color grading, screen distortion (
EndCaptureDraw/PostEndCaptureDraw) - Custom drawing between tiles / players / entities (
DrawBeforeTiles…DrawAfterEntities) - Built-in subsystems:
PRTRender(particles),Model3DRenderer(3D),ActorRender(Actor layered draw)
Not for:
- Server logic or network sync →
ModSystem/ SyncVar Network Sync - Regular HUD / panels → Basic UI
UIHandle(separate pipeline, not RenderHandle) - Single projectile / NPC
PostDrawsprite → useMain.spriteBatchdirectly; no RenderHandle needed
| Module | Relationship to RenderHandle |
|---|---|
| Basic PRT System |
PRTRender : RenderHandle, Weight = 1; PRTRenderLayer aligns to draw stages |
| Models3D System |
Model3DRenderer : RenderHandle, Weight = 5; Model3DLayer aligns to draw stages |
| Actor Entity System | Internal ActorRender : RenderHandle, draws by ActorDrawLayer
|
| Basic UI |
No direct link; UIHandle uses its own layer stack and LogicUpdate
|
using InnoVault.RenderHandles;
using Microsoft.Xna.Framework.Graphics;
using Terraria;
using Terraria.ModLoader;
// Autoload: ModType subclasses of RenderHandle are registered by tModLoader
public class MyScreenTint : RenderHandle
{
public override bool CanLoad() => !Main.dedServ;
// Higher Weight = later in the chain; built-in PRT=1, Models3D=5
public override float Weight => 10f;
public override void EndCaptureDraw(
SpriteBatch spriteBatch,
GraphicsDevice graphicsDevice,
RenderTarget2D screenSwap)
{
// During EndCapture you can read filterManager / finalTexture / screenTarget1/2
// SpriteBatch state is fully managed by the implementer
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend);
// ... post-process Main.screenTarget or finalTexture ...
spriteBatch.End();
}
}Key points:
- Override
CanLoad()to skip registration on dedicated server (recommended) - Each hook has a SpriteBatch active/inactive contract — see API reference below
- Modify the visible frame via
Main.screenTarget;screenSwapis an intermediate buffer
| RenderHandle | Direct Main.spriteBatch |
ModSystem draw hooks | |
|---|---|---|---|
| Hook timing | 10+ standard stages pre-mapped | Find PostDraw / IL hooks yourself |
PostDrawTiles, etc. — limited stages |
| Multi-mod cooperation |
Instances sorted by Weight
|
Easy to clobber RenderState | No unified ordering |
| RT management | Auto ScreenSwap + ScreenTargets[]
|
Manual alloc/dispose | Manual |
| Post-process |
EndCaptureDraw with FilterManager RTs |
Must hook EndCapture yourself |
Usually not possible |
| Learning curve | Must learn SpriteBatch contracts per stage | Low, but state bugs common | Medium |
| Typical use | Full-screen FX, cross-system layered draw | Single entity PostDraw | Simple global overlay |
flowchart TB
subgraph load [Load phase]
ModType[ModType subclass autoload] --> VR[VaultRegister]
VR --> Inst[RenderHandle.Instances.Add]
Inst --> Sort[Sort by Weight ascending]
RHL[RenderHandleLoader IVaultLoader] --> Hooks[Attach Terraria On_ hooks]
end
subgraph frame [Each frame]
RHS[RenderHandleSystem.PostUpdateEverything] --> Upd[UpdateBySystem per instance]
Hooks --> Stage[DrawBatch per stage]
Stage --> H1[Instances Weight low to high]
H1 --> H2[HandleRenderAction error isolation]
end
subgraph unload [Unload]
RHL --> Dispose[DisposeScreenTargets + Instances.Clear]
end
flowchart TD
A[DrawBeforeTiles] --> B[DrawNPCsOverTiles]
B --> C[DrawAfterTiles]
C --> D[DrawBeforePlayers]
D --> E[Player draw orig]
E --> F[DrawAfterPlayers]
F --> G[DrawBeforeInfernoRings]
G --> H[Entities / particles etc.]
H --> I[EndEntityDraw / DrawAfterEntities]
I --> J[EndCaptureDraw]
J --> K[PostEndCaptureDraw]
K --> L[FilterManager.orig_EndCapture]
EndCaptureDrawis skipped whenMain.gameMenu == true;PostEndCaptureDrawstill runs on the main menu.
| Type | Weight | Notes |
|---|---|---|
PRTRender |
1 |
Particle layered draw, runs first |
ActorRender (internal) |
default 1
|
Actor layered draw |
Model3DRenderer |
5 |
3D models, after PRT to avoid clobbering particle canvas |
| Custom | suggest > 5 or < 1
|
Insert where needed |
Draw a simple overlay in DrawAfterEntities (SpriteBatch inactive on entry; must End before return):
public class DebugOverlay : RenderHandle
{
public override bool CanLoad() => !Main.dedServ;
public override void DrawAfterEntities(
SpriteBatch spriteBatch,
GraphicsDevice graphicsDevice,
RenderTarget2D screenSwap)
{
spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.AlphaBlend,
Main.DefaultSamplerState, DepthStencilState.None,
Main.Rasterizer, null, Main.GameViewMatrix.TransformationMatrix);
Utils.DrawBorderString(spriteBatch, "RenderHandle OK",
Main.LocalPlayer.Center - Main.screenPosition, Color.Lime);
spriteBatch.End();
}
}public class VignettePass : RenderHandle
{
public override bool CanLoad() => !Main.dedServ;
public override float Weight => 20f;
public override void EndCaptureDraw(
SpriteBatch spriteBatch,
GraphicsDevice graphicsDevice,
RenderTarget2D screenSwap)
{
// Readable during this stage:
// filterManager, finalTexture, screenTarget1, screenTarget2
// Target Main.screenTarget for actual frame modifications
spriteBatch.Begin(/* ... */);
// Draw vignette texture to screenTarget
spriteBatch.End();
}
}public class BlurPass : RenderHandle
{
public override bool CanLoad() => !Main.dedServ;
public override int ScreenSlot => 2; // Two full-screen RTs
public override void DrawAfterPlayers(
SpriteBatch spriteBatch,
GraphicsDevice graphicsDevice,
RenderTarget2D screenSwap)
{
RenderTarget2D temp = ScreenTargets[0];
graphicsDevice.SetRenderTarget(temp);
graphicsDevice.Clear(Color.Transparent);
// ... render to temp ...
graphicsDevice.SetRenderTarget(null);
// Composite back to main frame
}
}Do not set
ScreenSlottoo high — each RT costsscreenWidth × screenHeightVRAM.
public override void OnResolutionChanged(Vector2 screenSize) {
// ScreenTargets are rebuilt by the framework; drop cached textures or reset layout here
}Custom RenderHandle subclasses can call Model3DRenderer.DrawInstance, BuildWorldMatrix, etc. to reuse OBJ/glTF loading and matrix helpers. See Models3D System Level 3.
For special cases (mind SpriteBatch contracts):
// Entry: Active; exit: still Active
PRTRender.DrawAll(spriteBatch);
// Entry: inactive; exit: still inactive
PRTRender.DrawLayer(spriteBatch, PRTRenderLayer.AfterTiles);When RenderHandleLoader.HandleRenderAction catches an exception:
- Sets
ignoreBug = 60(~1 second skip for that instance) - Increments
errorCountand logs / on-screen red text
The instance resumes automatically after the cooldown once the bug is fixed.
| Member | Type | Default | Description |
|---|---|---|---|
Weight |
float |
1 |
Sort weight; higher = later |
ScreenSlot |
int |
0 |
Private RT count; creates ScreenTargets when > 0
|
ScreenTargets |
RenderTarget2D[] |
— | Paired with ScreenSlot
|
filterManager |
FilterManager |
— | Valid only during EndCapture*
|
finalTexture |
RenderTarget2D |
— | Valid only during EndCapture*
|
screenTarget1/2 |
RenderTarget2D |
— | Valid only during EndCapture*
|
Instances |
List<RenderHandle> |
— | All registered instances (Weight-sorted) |
| Method | When |
|---|---|
VaultRegister() |
Added to Instances; optional ScreenTargets creation |
VaultSetup() / SetStaticDefaults()
|
Content setup |
UpdateBySystem(int index) |
Each frame in PostUpdateEverything (not on server) |
OnResolutionChanged(Vector2) |
Window resolution changed |
CreateScreenTargets() / DisposeScreenTargets()
|
Manual RT rebuild / release |
| Method | SpriteBatch on entry | Before return |
|---|---|---|
DrawBeforeTiles |
Active | Keep Active |
DrawNPCsOverTiles |
Caller-dependent | Must End |
DrawAfterTiles |
Inactive | Must End |
DrawBeforePlayers |
Inactive | Must End |
DrawAfterPlayers |
Inactive | Must End |
DrawBeforeInfernoRings |
Active | Keep Active |
DrawAfterEntities |
Inactive | Must End |
EndEntityDraw(sb, main) |
Inactive | Must End |
EndEntityDraw(sb, main, gd, swap) |
Inactive | Must End |
EndCaptureDraw |
Self-managed | Self-managed |
PostEndCaptureDraw |
Self-managed | Self-managed |
Breaking the contract corrupts SpriteBatch state for later instances or vanilla draw — follow
PRTRendersource forEnd/Beginpairing.
| Type | Description |
|---|---|
RenderHandleLoader |
IVaultLoader: Terraria hooks, maintains ScreenSwap
|
RenderHandleSystem |
ModSystem: drives UpdateBySystem
|
RenderHandleLoader.ScreenSwap |
Global intermediate RT, similar to Main.screenTargetSwap
|
| Issue | Cause / fix |
|---|---|
| Black / garbled frame after draw | Wrong Begin/End contract; see table and PRTRender
|
| Dedicated server crash | Override CanLoad() => !Main.dedServ
|
| Order conflict with PRT / 3D | Adjust Weight; Models3D defaults to 5 after PRT |
ScreenTargets is null |
ScreenSlot == 0 allocates none; or not yet created on main thread |
| Main-menu post-process missing |
EndCaptureDraw skipped on menu; use PostEndCaptureDraw
|
| Instance suddenly stops drawing | Draw threw → ignoreBug cooldown; check [RenderHandleLoader:...] logs |
| Draw UI from RenderHandle? | Not recommended; use UIHandle — different timing |
| Need more Terraria stages? | Pipeline is fixed; open an issue or IL hook (not recommended alongside framework) |
-
Basic PRT System —
PRTRenderreference implementation -
Models3D System —
Model3DRendererand RT post-process - Basic UI — HUD / panels (separate pipeline)
- Actor Entity System — internal layered draw
| Previous | Home | Next |
|---|---|---|
| Basic PRT System | Home | Basic TP Entity |