cnq3/code/renderer/crp_main.cpp

677 lines
20 KiB
C++

/*
===========================================================================
Copyright (C) 2023-2024 Gian 'myT' Schellenbaum
This file is part of Challenge Quake 3 (CNQ3).
Challenge Quake 3 is free software; you can redistribute it
and/or modify it under the terms of the GNU General Public License as
published by the Free Software Foundation; either version 2 of the License,
or (at your option) any later version.
Challenge Quake 3 is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Challenge Quake 3. If not, see <https://www.gnu.org/licenses/>.
===========================================================================
*/
// Cinematic Rendering Pipeline - main interface
#include "crp_local.h"
#include "../client/cl_imgui.h"
#include "shaders/crp/oit.h.hlsli"
namespace blit
{
#include "compshaders/crp/blit_vs.h"
#include "compshaders/crp/blit_ps.h"
}
namespace ui
{
#include "compshaders/crp/ui_vs.h"
#include "compshaders/crp/ui_ps.h"
}
namespace imgui
{
#include "compshaders/crp/imgui_vs.h"
#include "compshaders/crp/imgui_ps.h"
}
namespace nuklear
{
#include "compshaders/crp/nuklear_vs.h"
#include "compshaders/crp/nuklear_ps.h"
}
namespace mip_1
{
#include "compshaders/crp/mip_1_cs.h"
}
namespace mip_2
{
#include "compshaders/crp/mip_2_cs.h"
}
namespace mip_3
{
#include "compshaders/crp/mip_3_cs.h"
}
CRP crp;
IRenderPipeline* crpp = &crp;
cvar_t* crp_dof;
cvar_t* crp_dof_overlay;
cvar_t* crp_dof_blades;
cvar_t* crp_dof_angle;
cvar_t* crp_gatherDof_focusNearDist;
cvar_t* crp_gatherDof_focusNearRange;
cvar_t* crp_gatherDof_focusFarDist;
cvar_t* crp_gatherDof_focusFarRange;
cvar_t* crp_gatherDof_brightness;
cvar_t* crp_accumDof_focusDist;
cvar_t* crp_accumDof_radius;
cvar_t* crp_accumDof_samples;
cvar_t* crp_accumDof_preview;
static const cvarTableItem_t crp_cvars[] =
{
{
&crp_dof, "crp_dof", "1", CVAR_ARCHIVE, CVART_INTEGER, "0", "2",
"enables depth of field\n"
S_COLOR_VAL " 0 " S_COLOR_HELP "= Disabled\n"
S_COLOR_VAL " 1 " S_COLOR_HELP "= Gather (fast, more flexible, issues with transparency)\n"
S_COLOR_VAL " 2 " S_COLOR_HELP "= Accumulation (slow, less flexible, great IQ)\n",
"DoF mode", CVARCAT_GRAPHICS, "Depth of field mode", "",
CVAR_GUI_VALUE("0", "Disabled", "")
CVAR_GUI_VALUE("1", "Gather", "Fast, lower IQ")
CVAR_GUI_VALUE("2", "Accumulation", "Very slow, great IQ")
},
{
&crp_dof_overlay, "crp_dof_overlay", "0", CVAR_ARCHIVE, CVART_INTEGER, "0", "2",
"debug overlay mode\n"
S_COLOR_VAL " 0 " S_COLOR_HELP "= Disabled\n"
S_COLOR_VAL " 1 " S_COLOR_HELP "= Colorized Blur\n"
S_COLOR_VAL " 2 " S_COLOR_HELP "= Focus Plane",
"DoF overlay mode", CVARCAT_GRAPHICS, "Debug overlay mode", "",
CVAR_GUI_VALUE("0", "Disabled", "")
CVAR_GUI_VALUE("1", "Colorized Blur", "")
CVAR_GUI_VALUE("2", "Focus Plane", "")
},
{
&crp_dof_blades, "crp_dof_blades", "6", CVAR_ARCHIVE, CVART_FLOAT, "0", "16",
"aperture blade count\n"
"Set to less than 3 for a disk shape.",
"DoF blade count", CVARCAT_GRAPHICS, "Aperture blade count", "Set to less than 3 for a disk shape."
},
{
&crp_dof_angle, "crp_dof_angle", "20", CVAR_ARCHIVE, CVART_FLOAT, "0", "360", "aperture angle, in degrees",
"DoF aperture angle", CVARCAT_GRAPHICS, "Aperture angle, in degrees", ""
},
{
&crp_accumDof_focusDist, "crp_accumDof_focusDist", "256", CVAR_ARCHIVE, CVART_FLOAT, "2", "2048", "focus distance",
"Accum DoF focus distance", CVARCAT_GRAPHICS, "Focus distance", ""
},
{
&crp_accumDof_radius, "crp_accumDof_blurRadius", "0.1", CVAR_ARCHIVE, CVART_FLOAT, "0.001", "20", "aperture radius in world units",
"Accum DoF aperture radius", CVARCAT_GRAPHICS, "Aperture radius in world units", ""
},
{
&crp_accumDof_samples, "crp_accumDof_samples", "2", CVAR_ARCHIVE, CVART_INTEGER, "1", "12",
"per-axis sampling density\n"
"Density N means (2N + 1)(2N + 1) scene renders in total.",
"Accum DoF sample count", CVARCAT_GRAPHICS, "Per-axis sampling density", "Density N means (2N + 1)^2 scene renders in total."
},
{
&crp_accumDof_preview, "crp_accumDof_preview", "0", CVAR_ARCHIVE, CVART_INTEGER, "0", "2",
"low-res preview mode\n"
S_COLOR_VAL " 0 " S_COLOR_HELP "= Disabled\n"
S_COLOR_VAL " 1 " S_COLOR_HELP "= 1/4 pixel count, 9 samples total\n"
S_COLOR_VAL " 2 " S_COLOR_HELP "= 1/16 pixel count, 25 samples total",
"Accum DoF preview mode", CVARCAT_GRAPHICS, "Low-resolution preview modes", "",
CVAR_GUI_VALUE("0", "Disabled", "")
CVAR_GUI_VALUE("1", "1/4 pixel count", "9 samples total")
CVAR_GUI_VALUE("2", "1/16 pixel count", "25 samples total")
},
{
&crp_gatherDof_focusNearDist, "crp_gatherDof_focusNearDist", "192", CVAR_ARCHIVE, CVART_FLOAT, "1", "2048", "near focus distance",
"Gather DoF near focus distance", CVARCAT_GRAPHICS, "Near focus distance", ""
},
{
&crp_gatherDof_focusNearRange, "crp_gatherDof_focusNearRange", "256", CVAR_ARCHIVE, CVART_FLOAT, "1", "2048", "near focus range",
"Gather DoF near focus range", CVARCAT_GRAPHICS, "Near focus range", ""
},
{
&crp_gatherDof_focusFarDist, "crp_gatherDof_focusFarDist", "512", CVAR_ARCHIVE, CVART_FLOAT, "1", "2048", "far focus distance",
"Gather DoF far focus distance", CVARCAT_GRAPHICS, "Far focus distance", ""
},
{
&crp_gatherDof_focusFarRange, "crp_gatherDof_focusFarRange", "384", CVAR_ARCHIVE, CVART_FLOAT, "1", "2048", "far focus range",
"Gather DoF far focus range", CVARCAT_GRAPHICS, "Far focus range", ""
},
{
&crp_gatherDof_brightness, "crp_gatherDof_brightness", "2", CVAR_ARCHIVE, CVART_FLOAT, "0", "8", "blur brightness weight",
"Gather DoF bokeh brightness", CVARCAT_GRAPHICS, "Blur brightness weight", ""
}
};
void PSOCache::Init(Entry* entries_, uint32_t maxEntryCount_)
{
entries = entries_;
maxEntryCount = maxEntryCount_;
entryCount = 1; // we treat index 0 as invalid
}
int PSOCache::AddPipeline(const GraphicsPipelineDesc& desc, const char* name)
{
// we treat index 0 as invalid, so start at 1
for(uint32_t i = 1; i < entryCount; ++i)
{
Entry& entry = entries[i];
if(memcmp(&entry.desc, &desc, sizeof(desc)) == 0)
{
return (int)i;
}
}
ASSERT_OR_DIE(entryCount < maxEntryCount, "Not enough entries in the PSO cache");
GraphicsPipelineDesc namedDesc = desc;
namedDesc.name = name;
const uint32_t index = entryCount++;
Entry& entry = entries[index];
entry.desc = desc; // keep the original desc for proper comparison results
entry.handle = CreateGraphicsPipeline(namedDesc);
return (int)index;
}
void CRP::Init()
{
static bool veryFirstInit = true;
if(veryFirstInit)
{
ri.Cvar_RegisterTable(crp_cvars, ARRAY_LEN(crp_cvars));
veryFirstInit = false;
}
InitDesc initDesc;
initDesc.directDescriptorHeapIndexing = true;
srp.firstInit = RHI::Init(initDesc);
srp.psoStatsValid = false;
if(srp.firstInit)
{
srp.CreateShaderTraceBuffers();
for(uint32_t f = 0; f < FrameCount; ++f)
{
// the doubled index count is for the depth pre-pass
const int MaxDynamicVertexCount = 16 << 20;
const int MaxDynamicIndexCount = MaxDynamicVertexCount * 4;
GeoBuffers& db = dynBuffers[f];
db.Create(va("world #%d", f + 1), MaxDynamicVertexCount, MaxDynamicIndexCount);
}
}
// we recreate the samplers on every vid_restart to create the right level
// of anisotropy based on the latched CVar
for(uint32_t w = 0; w < TW_COUNT; ++w)
{
for(uint32_t f = 0; f < TextureFilter::Count; ++f)
{
for(uint32_t m = 0; m < MaxTextureMips; ++m)
{
const textureWrap_t wrap = (textureWrap_t)w;
const TextureFilter::Id filter = (TextureFilter::Id)f;
const uint32_t s = GetBaseSamplerIndex(wrap, filter, m);
SamplerDesc desc(wrap, filter, (float)m);
desc.shortLifeTime = true;
samplers[s] = CreateSampler(desc);
samplerIndices[s] = RHI::GetSamplerIndex(samplers[s]);
}
}
}
{
renderTargetFormat = TextureFormat::RGBA64_Float;
TextureDesc desc("render target #1", glConfig.vidWidth, glConfig.vidHeight);
desc.initialState = ResourceStates::RenderTargetBit;
desc.allowedState = ResourceStates::RenderTargetBit | ResourceStates::PixelShaderAccessBit;
Vector4Clear(desc.clearColor);
desc.usePreferredClearValue = true;
desc.committedResource = true;
desc.format = renderTargetFormat;
desc.shortLifeTime = true;
renderTargets[0] = RHI::CreateTexture(desc);
desc.name = "render target #2";
renderTargets[1] = RHI::CreateTexture(desc);
renderTargetIndex = 0;
renderTarget = renderTargets[0];
}
{
TextureDesc desc("readback render target", glConfig.vidWidth, glConfig.vidHeight);
desc.initialState = ResourceStates::RenderTargetBit;
desc.allowedState = ResourceStates::RenderTargetBit | ResourceStates::PixelShaderAccessBit;
Vector4Clear(desc.clearColor);
desc.usePreferredClearValue = true;
desc.committedResource = true;
desc.format = TextureFormat::RGBA32_UNorm;
desc.shortLifeTime = true;
readbackRenderTarget = RHI::CreateTexture(desc);
}
{
TextureDesc desc("OIT index", glConfig.vidWidth, glConfig.vidHeight);
desc.initialState = ResourceStates::UnorderedAccessBit;
desc.allowedState = ResourceStates::UnorderedAccessBit | ResourceStates::PixelShaderAccessBit | ResourceStates::ComputeShaderAccessBit;
desc.committedResource = true;
desc.format = TextureFormat::R32_UInt;
desc.shortLifeTime = true;
oitIndexTexture = RHI::CreateTexture(desc);
}
uint32_t oitMaxFragmentCount = 0;
{
const int byteCountPerFragment = sizeof(OIT_Fragment);
const int fragmentCount = glConfig.vidWidth * glConfig.vidHeight * OIT_AVG_FRAGMENTS_PER_PIXEL;
const int byteCount = byteCountPerFragment * fragmentCount;
oitMaxFragmentCount = fragmentCount;
BufferDesc desc("OIT fragment", byteCount, ResourceStates::UnorderedAccessBit);
desc.committedResource = true;
desc.memoryUsage = MemoryUsage::GPU;
desc.structureByteCount = byteCountPerFragment;
desc.shortLifeTime = true;
oitFragmentBuffer = CreateBuffer(desc);
}
{
const int byteCount = sizeof(OIT_Counter);
{
BufferDesc desc("OIT counter", byteCount, ResourceStates::UnorderedAccessBit);
desc.committedResource = true;
desc.memoryUsage = MemoryUsage::GPU;
desc.structureByteCount = byteCount;
desc.shortLifeTime = true;
oitCounterBuffer = CreateBuffer(desc);
}
{
BufferDesc desc("OIT counter staging", byteCount, ResourceStates::Common);
desc.committedResource = false;
desc.memoryUsage = MemoryUsage::Upload;
desc.structureByteCount = byteCount;
desc.shortLifeTime = true;
oitCounterStagingBuffer = CreateBuffer(desc);
uint32_t* dst = (uint32_t*)MapBuffer(oitCounterStagingBuffer);
dst[0] = 1; // fragment index 0 is the end-of-list value
dst[1] = oitMaxFragmentCount;
dst[2] = 0;
UnmapBuffer(oitCounterStagingBuffer);
}
}
{
TextureDesc desc("depth buffer", glConfig.vidWidth, glConfig.vidHeight);
desc.committedResource = true;
desc.shortLifeTime = true;
desc.initialState = ResourceStates::DepthWriteBit;
desc.allowedState = ResourceStates::DepthAccessBits | ResourceStates::PixelShaderAccessBit;
desc.format = TextureFormat::Depth32_Float;
desc.SetClearDepthStencil(0.0f, 0);
depthTexture = RHI::CreateTexture(desc);
}
{
GraphicsPipelineDesc desc("blit LDR");
desc.vertexShader = ShaderByteCode(blit::g_vs);
desc.pixelShader = ShaderByteCode(blit::g_ps);
desc.depthStencil.DisableDepth();
desc.rasterizer.cullMode = CT_TWO_SIDED;
desc.AddRenderTarget(0, TextureFormat::RGBA32_UNorm);
blitPipelineLDR = CreateGraphicsPipeline(desc);
desc.name = "blit HDR";
desc.renderTargets[0].format = TextureFormat::RGBA64_Float;
blitPipelineHDR = CreateGraphicsPipeline(desc);
}
ui.Init(true, ShaderByteCode(ui::g_vs), ShaderByteCode(ui::g_ps), renderTargetFormat, RHI_MAKE_NULL_HANDLE(), NULL);
imgui.Init(true, ShaderByteCode(imgui::g_vs), ShaderByteCode(imgui::g_ps), renderTargetFormat, RHI_MAKE_NULL_HANDLE(), NULL);
nuklear.Init(true, ShaderByteCode(nuklear::g_vs), ShaderByteCode(nuklear::g_ps), renderTargetFormat, RHI_MAKE_NULL_HANDLE(), NULL);
mipMapGen.Init(true, ShaderByteCode(mip_1::g_cs), ShaderByteCode(mip_2::g_cs), ShaderByteCode(mip_3::g_cs));
opaque.Init();
transp.Init();
transpResolve.Init();
toneMap.Init();
gatherDof.Init();
accumDof.Init();
fog.Init();
srp.firstInit = false;
}
void CRP::ShutDown(bool fullShutDown)
{
RHI::ShutDown(fullShutDown);
}
void CRP::BeginFrame()
{
renderTargetIndex = 0;
renderTarget = renderTargets[0];
srp.BeginFrame();
// have it be first to we can use ImGUI in the other components too
imgui.BeginFrame();
RHI::BeginFrame();
ui.BeginFrame();
nuklear.BeginFrame();
const float clearColor[4] = { 0.0f, 0.5f, 0.0f, 0.0f };
const TextureBarrier barrier(renderTarget, ResourceStates::RenderTargetBit);
CmdBarrier(1, &barrier);
CmdClearColorTarget(renderTarget, clearColor);
frameSeed = (float)rand() / (float)RAND_MAX;
dynBuffers[GetFrameIndex()].Rewind();
}
void CRP::EndFrame()
{
srp.DrawGUI();
imgui.Draw(renderTarget);
toneMap.DrawToneMap();
BlitRenderTarget(GetSwapChainTexture(), "Blit to Swap Chain");
BlitRenderTarget(readbackRenderTarget, "Blit to Readback Texture");
srp.EndFrame();
}
void CRP::Blit(HTexture destination, HTexture source, const char* passName, bool hdr, const vec2_t tcScale, const vec2_t tcBias)
{
SCOPED_RENDER_PASS(passName, 0.125f, 0.125f, 0.5f);
const TextureBarrier barriers[2] =
{
TextureBarrier(source, ResourceStates::PixelShaderAccessBit),
TextureBarrier(destination, ResourceStates::RenderTargetBit)
};
CmdBarrier(ARRAY_LEN(barriers), barriers);
#pragma pack(push, 4)
struct BlitRC
{
uint32_t textureIndex;
uint32_t samplerIndex;
float tcScale[2];
float tcBias[2];
};
#pragma pack(pop)
BlitRC rc;
rc.textureIndex = GetTextureIndexSRV(source);
rc.samplerIndex = GetSamplerIndex(TW_CLAMP_TO_EDGE, TextureFilter::Linear);
rc.tcScale[0] = tcScale[0];
rc.tcScale[1] = tcScale[1];
rc.tcBias[0] = tcBias[0];
rc.tcBias[1] = tcBias[0];
CmdSetViewportAndScissor(0, 0, glConfig.vidWidth, glConfig.vidHeight);
CmdBindRenderTargets(1, &destination, NULL);
CmdBindPipeline(hdr ? blitPipelineHDR : blitPipelineLDR);
CmdSetGraphicsRootConstants(0, sizeof(rc), &rc);
CmdDraw(3, 0);
}
void CRP::BlitRenderTarget(HTexture destination, const char* passName)
{
Blit(destination, crp.renderTarget, passName, false, vec2_one, vec2_zero);
}
void CRP::CreateTexture(image_t* image, int mipCount, int width, int height)
{
TextureDesc desc(image->name, width, height, mipCount);
desc.committedResource = width * height >= (1 << 20);
desc.shortLifeTime = true;
if(mipCount > 1)
{
desc.allowedState |= ResourceStates::UnorderedAccessBit; // for mip-map generation
}
image->texture = ::RHI::CreateTexture(desc);
image->textureIndex = GetTextureIndexSRV(image->texture);
}
void CRP::UpoadTextureAndGenerateMipMaps(image_t* image, const byte* data)
{
MappedTexture texture;
RHI::BeginTextureUpload(texture, image->texture);
for(uint32_t r = 0; r < texture.rowCount; ++r)
{
memcpy(texture.mappedData + r * texture.dstRowByteCount, data + r * texture.srcRowByteCount, texture.srcRowByteCount);
}
RHI::EndTextureUpload();
mipMapGen.GenerateMipMaps(image->texture);
}
void CRP::BeginTextureUpload(MappedTexture& mappedTexture, image_t* image)
{
RHI::BeginTextureUpload(mappedTexture, image->texture);
}
void CRP::EndTextureUpload()
{
RHI::EndTextureUpload();
}
void CRP::ProcessWorld(world_t&)
{
}
void CRP::ProcessModel(model_t&)
{
}
void CRP::ProcessShader(shader_t& shader)
{
if(shader.isOpaque)
{
opaque.ProcessShader(shader);
}
else
{
transp.ProcessShader(shader);
}
}
void CRP::ExecuteRenderCommands(const byte* data, bool /*readbackRequested*/)
{
// @NOTE: the CRP always blits the final result to the readback texture
for(;;)
{
const int commandId = ((const renderCommandBase_t*)data)->commandId;
if(commandId < 0 || commandId >= RC_COUNT)
{
assert(!"Invalid render command type");
return;
}
if(commandId == RC_END_OF_LIST)
{
return;
}
switch(commandId)
{
case RC_UI_SET_COLOR:
ui.CmdSetColor(*(const uiSetColorCommand_t*)data);
break;
case RC_UI_DRAW_QUAD:
ui.CmdDrawQuad(*(const uiDrawQuadCommand_t*)data);
break;
case RC_UI_DRAW_TRIANGLE:
ui.CmdDrawTriangle(*(const uiDrawTriangleCommand_t*)data);
break;
case RC_DRAW_SCENE_VIEW:
DrawSceneView(*(const drawSceneViewCommand_t*)data);
break;
case RC_BEGIN_FRAME:
BeginFrame();
break;
case RC_SWAP_BUFFERS:
EndFrame();
break;
case RC_BEGIN_UI:
ui.Begin(renderTarget);
break;
case RC_END_UI:
ui.End();
break;
case RC_BEGIN_3D:
// @TODO:
srp.renderMode = RenderMode::None;
break;
case RC_END_3D:
// @TODO:
srp.renderMode = RenderMode::None;
break;
case RC_END_SCENE:
// @TODO: post-processing
break;
case RC_BEGIN_NK:
nuklear.Begin(renderTarget);
break;
case RC_END_NK:
nuklear.End();
break;
case RC_NK_UPLOAD:
nuklear.Upload(*(const nuklearUploadCommand_t*)data);
break;
case RC_NK_DRAW:
nuklear.Draw(*(const nuklearDrawCommand_t*)data);
break;
default:
Q_assert(!"Unsupported render command type");
return;
}
data += renderCommandSizes[commandId];
}
}
void CRP::TessellationOverflow()
{
switch(tess.tessellator)
{
case Tessellator::Opaque: opaque.TessellationOverflow(); break;
case Tessellator::Transp: transp.TessellationOverflow(); break;
default: break;
}
tess.numIndexes = 0;
tess.numVertexes = 0;
}
void CRP::DrawSceneView(const drawSceneViewCommand_t& cmd)
{
const viewParms_t& vp = cmd.viewParms;
if(cmd.shouldClearColor)
{
const Rect rect(vp.viewportX, vp.viewportY, vp.viewportWidth, vp.viewportHeight);
const TextureBarrier tb(renderTarget, ResourceStates::RenderTargetBit);
CmdBarrier(1, &tb);
CmdClearColorTarget(renderTarget, cmd.clearColor, &rect);
}
if(cmd.numDrawSurfs <= 0 || !cmd.shouldDrawScene)
{
return;
}
if(crp_dof->integer == DOFMethod::Accumulation &&
vp.viewportX == 0 &&
vp.viewportY == 0 &&
vp.viewportWidth == glConfig.vidWidth &&
vp.viewportHeight == glConfig.vidHeight)
{
const Rect rect(0, 0, glConfig.vidWidth, glConfig.vidHeight);
accumDof.Begin(cmd);
const uint32_t sampleCount = accumDof.GetSampleCount();
for(uint32_t y = 0; y < sampleCount; y++)
{
for(uint32_t x = 0; x < sampleCount; x++)
{
srp.enableRenderPassQueries = x == 0 && y == 0;
drawSceneViewCommand_t newCmd;
accumDof.FixCommand(newCmd, cmd, x, y);
const TextureBarrier tb(renderTarget, ResourceStates::RenderTargetBit);
CmdBarrier(1, &tb);
CmdClearColorTarget(renderTarget, cmd.clearColor, &rect);
opaque.Draw(newCmd);
fog.Draw();
transp.Draw(newCmd);
transpResolve.Draw(newCmd);
accumDof.Accumulate();
// geometry allocation is a linear allocation instead of a ring buffer
// we force a CPU-GPU sync point after every full scene render
// that way, we can keep the buffer sizes at least somewhat reasonable
SubmitAndContinue();
dynBuffers[GetFrameIndex()].Rewind();
}
}
CmdSetViewportAndScissor(backEnd.viewParms);
srp.enableRenderPassQueries = true;
accumDof.Normalize();
backEnd.viewParms = cmd.viewParms;
backEnd.refdef = cmd.refdef;
accumDof.DrawDebug();
}
else
{
opaque.Draw(cmd);
fog.Draw();
transp.Draw(cmd);
transpResolve.Draw(cmd);
CmdSetViewportAndScissor(vp.viewportX, vp.viewportY, vp.viewportWidth, vp.viewportHeight);
gatherDof.Draw();
}
}
void CRP::ReadPixels(int w, int h, int alignment, colorSpace_t colorSpace, void* outPixels)
{
ReadTextureImage(outPixels, readbackRenderTarget, w, h, alignment, colorSpace);
}
uint32_t CRP::GetSamplerDescriptorIndexFromBaseIndex(uint32_t baseIndex)
{
Q_assert(baseIndex < ARRAY_LEN(samplerIndices));
return samplerIndices[baseIndex];
}
HTexture CRP::GetReadRenderTarget()
{
return renderTargets[renderTargetIndex ^ 1];
}
HTexture CRP::GetWriteRenderTarget()
{
return renderTargets[renderTargetIndex];
}
void CRP::SwapRenderTargets()
{
renderTargetIndex ^= 1;
renderTarget = GetWriteRenderTarget();
}