cnq3/code/renderer/srp_main.cpp

543 lines
14 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/>.
===========================================================================
*/
// Shared Rendering Pipeline - core functionality
#include "srp_local.h"
#include "../client/cl_imgui.h"
#include "shaders/crp/alpha_test.h.hlsli"
extern IRenderPipeline* grpp;
extern IRenderPipeline* crpp;
SRP srp;
IRenderPipeline* renderPipeline;
static ImPlotPoint FrameTimeGetter(int index, void*)
{
const FrameStats& fs = srp.frameStats;
const int realIndex = (fs.frameIndex + index) % fs.frameCount;
const float value = fs.p2pMS[realIndex];
ImPlotPoint p;
p.x = index;
p.y = value;
return p;
}
static void UpdateAnimatedImage(image_t* image, int w, int h, const byte* data, qbool dirty)
{
if(w != image->width || h != image->height)
{
// @TODO: ?
/*image->width = w;
image->height = h;
CreateTexture(&d3d.textures[image->texnum], image, 1, w, h);
GAL_UpdateTexture(image, 0, 0, 0, w, h, data);*/
}
else if(dirty)
{
// @TODO: ?
//GAL_UpdateTexture(image, 0, 0, 0, w, h, data);
}
}
const image_t* GetBundleImage(const textureBundle_t& bundle)
{
return R_UpdateAndGetBundleImage(&bundle, &UpdateAnimatedImage);
}
uint32_t GetBaseSamplerIndex(textureWrap_t wrap, TextureFilter::Id filter, uint32_t minLOD)
{
Q_assert((uint32_t)wrap < TW_COUNT);
Q_assert((uint32_t)filter < TextureFilter::Count);
const uint32_t baseIndex =
(uint32_t)filter +
(uint32_t)TextureFilter::Count * (uint32_t)wrap +
(uint32_t)TextureFilter::Count * (uint32_t)TW_COUNT * minLOD;
return baseIndex;
}
uint32_t GetSamplerIndex(textureWrap_t wrap, TextureFilter::Id filter, uint32_t minLOD)
{
const uint32_t baseIndex = GetBaseSamplerIndex(wrap, filter, minLOD);
const uint32_t descIndex = renderPipeline->GetSamplerDescriptorIndexFromBaseIndex(baseIndex);
return descIndex;
}
uint32_t GetSamplerIndex(const image_t* image)
{
TextureFilter::Id filter = TextureFilter::Anisotropic;
if(r_lego->integer &&
srp.renderMode == RenderMode::World &&
(image->flags & (IMG_LMATLAS | IMG_EXTLMATLAS | IMG_NOPICMIP)) == 0)
{
filter = TextureFilter::Point;
}
else if((image->flags & IMG_NOAF) != 0 ||
srp.renderMode != RenderMode::World)
{
filter = TextureFilter::Linear;
}
int minLOD = 0;
if(srp.renderMode == RenderMode::World &&
(image->flags & IMG_NOPICMIP) == 0)
{
minLOD = Com_ClampInt(0, MaxTextureMips - 1, r_picmip->integer);
}
return GetSamplerIndex(image->wrapClampMode, filter, (uint32_t)minLOD);
}
void ReadTextureImage(void* outPixels, HTexture hreadback, int w, int h, int alignment, colorSpace_t colorSpace)
{
MappedTexture mapped;
BeginTextureReadback(mapped, hreadback);
byte* const out0 = (byte*)outPixels;
const byte* const in0 = mapped.mappedData;
if(colorSpace == CS_RGBA)
{
const int dstRowSizeNoPadding = w * 4;
mapped.dstRowByteCount = AlignUp(dstRowSizeNoPadding, alignment);
for(int y = 0; y < mapped.rowCount; ++y)
{
byte* out = out0 + (mapped.rowCount - 1 - y) * mapped.dstRowByteCount;
const byte* in = in0 + y * mapped.srcRowByteCount;
memcpy(out, in, dstRowSizeNoPadding);
}
}
else if(colorSpace == CS_BGR)
{
mapped.dstRowByteCount = AlignUp(w * 3, alignment);
for(int y = 0; y < mapped.rowCount; ++y)
{
byte* out = out0 + (mapped.rowCount - 1 - y) * mapped.dstRowByteCount;
const byte* in = in0 + y * mapped.srcRowByteCount;
for(int x = 0; x < mapped.columnCount; ++x)
{
out[2] = in[0];
out[1] = in[1];
out[0] = in[2];
out += 3;
in += 4;
}
}
}
else
{
Q_assert(!"Unsupported color space");
}
EndTextureReadback();
}
void UpdateEntityData(bool& depthHack, int entityNum, double originalTime)
{
depthHack = false;
if(entityNum != ENTITYNUM_WORLD)
{
backEnd.currentEntity = &backEnd.refdef.entities[entityNum];
if(backEnd.currentEntity->intShaderTime)
{
backEnd.refdef.floatTime = originalTime - (double)backEnd.currentEntity->e.shaderTime.iShaderTime / 1000.0;
}
else
{
backEnd.refdef.floatTime = originalTime - backEnd.currentEntity->e.shaderTime.fShaderTime;
}
// we have to reset the shaderTime as well otherwise image animations start
// from the wrong frame
tess.shaderTime = backEnd.refdef.floatTime - tess.shader->timeOffset;
// set up the transformation matrix
R_RotateForEntity(backEnd.currentEntity, &backEnd.viewParms, &backEnd.orient, backEnd.modelMatrix);
if(backEnd.currentEntity->e.renderfx & RF_DEPTHHACK)
{
depthHack = true;
}
}
else
{
backEnd.currentEntity = &tr.worldEntity;
backEnd.refdef.floatTime = originalTime;
backEnd.orient = backEnd.viewParms.world;
R_MakeIdentityMatrix(backEnd.modelMatrix);
// we have to reset the shaderTime as well otherwise image animations on
// the world (like water) continue with the wrong frame
tess.shaderTime = backEnd.refdef.floatTime - tess.shader->timeOffset;
}
}
cullType_t GetMirrorredCullType(cullType_t cullType)
{
switch(cullType)
{
case CT_BACK_SIDED: return CT_FRONT_SIDED;
case CT_FRONT_SIDED: return CT_BACK_SIDED;
default: return CT_TWO_SIDED;
}
}
uint32_t AlphaTestShaderConstFromStateBits(unsigned int stateBits)
{
stateBits &= GLS_ATEST_BITS;
switch(stateBits)
{
case GLS_ATEST_GE_80: return ATEST_GE_HALF;
case GLS_ATEST_GT_0: return ATEST_GT_0;
case GLS_ATEST_LT_80: return ATEST_LT_HALF;
default: return ATEST_NONE;
}
}
void R_SelectRenderPipeline()
{
if(r_pipeline->integer == 0)
{
renderPipeline = grpp;
FS_EnableCNQ3Folder(qfalse);
}
else
{
renderPipeline = crpp;
FS_EnableCNQ3Folder(qtrue);
}
}
void FrameStats::EndFrame()
{
frameCount = min(frameCount + 1, (int)MaxFrames);
frameIndex = (frameIndex + 1) % MaxFrames;
Com_StatsFromArray(p2pMS, frameCount, temp, &p2pStats);
}
void RenderPassStats::EndFrame(uint32_t cpu, uint32_t gpu)
{
static uint32_t tempSamples[MaxStatsFrameCount];
samplesCPU[index] = cpu;
samplesGPU[index] = gpu;
count = min(count + 1, (uint32_t)MaxStatsFrameCount);
index = (index + 1) % MaxStatsFrameCount;
Com_StatsFromArray((const int*)samplesCPU, count, (int*)tempSamples, &statsCPU);
Com_StatsFromArray((const int*)samplesGPU, count, (int*)tempSamples, &statsGPU);
}
uint32_t SRP::BeginRenderPass(const char* name, float r, float g, float b)
{
if(!enableRenderPassQueries)
{
CmdBeginDebugLabel(name, r, g, b);
return 0xDEADBEEF;
}
RenderPassFrame& f = renderPasses[tr.frameCount % FrameCount];
if(f.count >= ARRAY_LEN(f.passes))
{
Q_assert(0);
return UINT32_MAX;
}
CmdBeginDebugLabel(name, r, g, b);
const uint32_t index = f.count++;
RenderPassQueries& q = f.passes[index];
Q_strncpyz(q.name, name, sizeof(q.name));
q.cpuStartUS = Sys_Microseconds();
q.queryIndex = CmdBeginDurationQuery();
return index;
}
void SRP::EndRenderPass(uint32_t index)
{
if(!enableRenderPassQueries)
{
CmdEndDebugLabel();
return;
}
Q_assert(index != 0xDEADBEEF); // enableRenderPassQueries should still be false!
RenderPassFrame& f = renderPasses[tr.frameCount % FrameCount];
if(index >= f.count)
{
Q_assert(0);
return;
}
CmdEndDebugLabel();
RenderPassQueries& q = f.passes[index];
q.cpuDurationUS = (uint32_t)(Sys_Microseconds() - q.cpuStartUS);
CmdEndDurationQuery(q.queryIndex);
}
void SRP::DrawGUI()
{
uint32_t durations[MaxDurationQueries];
GetDurations(durations);
wholeFrameStats.EndFrame(rhie.renderToPresentUS, durations[0]);
const RenderPassFrame& currFrame = renderPasses[(tr.frameCount % FrameCount) ^ 1];
RenderPassFrame& tempFrame = tempRenderPasses;
// see if the render pass list is the same as the previous frame's
bool sameRenderPass = true;
if(currFrame.count == tempRenderPasses.count)
{
for(uint32_t p = 0; p < currFrame.count; ++p)
{
if(Q_stricmp(currFrame.passes[p].name, tempRenderPasses.passes[p].name) != 0)
{
sameRenderPass = false;
break;
}
}
}
else
{
sameRenderPass = false;
}
// write out the displayed timings into the temp buffer
tempFrame.count = currFrame.count;
if(sameRenderPass)
{
for(uint32_t p = 0; p < currFrame.count; ++p)
{
const uint32_t index = currFrame.passes[p].queryIndex;
if(index < MaxDurationQueries)
{
renderPassStats[p].EndFrame(currFrame.passes[p].cpuDurationUS, durations[index]);
tempFrame.passes[p].gpuDurationUS = renderPassStats[p].statsGPU.median;
tempFrame.passes[p].cpuDurationUS = renderPassStats[p].statsCPU.median;
}
}
}
else
{
for(uint32_t p = 0; p < currFrame.count; ++p)
{
const uint32_t index = currFrame.passes[p].queryIndex;
if(index < MaxDurationQueries)
{
tempFrame.passes[p].gpuDurationUS = durations[index];
tempFrame.passes[p].cpuDurationUS = currFrame.passes[p].cpuDurationUS;
}
}
}
static bool breakdownActive = false;
ToggleBooleanWithShortcut(breakdownActive, ImGuiKey_F);
GUI_AddMainMenuItem(GUI_MainMenu::Perf, "Frame breakdown", "Ctrl+F", &breakdownActive);
if(breakdownActive)
{
if(ImGui::Begin("Frame breakdown", &breakdownActive, ImGuiWindowFlags_AlwaysAutoResize))
{
if(BeginTable("Frame breakdown", 3))
{
TableHeader(3, "Pass", "GPU [us]", "CPU [us]");
TableRow(3, "Whole frame",
va("%d", (int)wholeFrameStats.statsGPU.median),
va("%d", (int)wholeFrameStats.statsCPU.median));
for(uint32_t p = 0; p < currFrame.count; ++p)
{
const RenderPassQueries& rp = tempFrame.passes[p];
if(rp.queryIndex < MaxDurationQueries)
{
TableRow(3, rp.name,
va("%d", (int)rp.gpuDurationUS),
va("%d", (int)rp.cpuDurationUS));
}
}
ImGui::EndTable();
}
if(psoStatsValid)
{
ImGui::Text("PSO count: %d", (int)psoCount);
ImGui::Text("PSO changes: %d", (int)psoChangeCount);
}
}
ImGui::End();
}
// save the current render pass list in the temp buffer
memcpy(&tempFrame, &currFrame, sizeof(tempFrame));
static bool frameTimeActive = false;
GUI_AddMainMenuItem(GUI_MainMenu::Perf, "Frame stats", NULL, &frameTimeActive);
if(frameTimeActive)
{
if(ImGui::Begin("Frame stats", &frameTimeActive, ImGuiWindowFlags_AlwaysAutoResize))
{
if(BeginTable("Frame stats", 2))
{
const FrameStats& fs = frameStats;
const stats_t& s = fs.p2pStats;
TableRow2("Skipped frames", fs.skippedFrames);
TableRow2("Frame time target", rhie.targetFrameDurationMS);
TableRow2("Frame time average", s.average);
TableRow2("Frame time std dev.", s.stdDev);
TableRow2("Input to render", (float)rhie.inputToRenderUS / 1000.0f);
TableRow2("Input to present", (float)rhie.inputToPresentUS / 1000.0f);
ImGui::EndTable();
}
}
ImGui::End();
}
static bool graphsActive = false;
ToggleBooleanWithShortcut(graphsActive, ImGuiKey_G);
GUI_AddMainMenuItem(GUI_MainMenu::Perf, "Frame time graphs", "Ctrl+G", &graphsActive);
if(graphsActive)
{
const int windowFlags =
ImGuiWindowFlags_NoDecoration |
ImGuiWindowFlags_NoBackground |
ImGuiWindowFlags_NoMove;
ImGui::SetNextWindowSize(ImVec2(glConfig.vidWidth, glConfig.vidHeight / 2), ImGuiCond_Always);
ImGui::SetNextWindowPos(ImVec2(0, glConfig.vidHeight / 2), ImGuiCond_Always);
if(ImGui::Begin("Frame time graphs", &graphsActive, windowFlags))
{
const FrameStats& fs = frameStats;
const double target = (double)rhie.targetFrameDurationMS;
static bool autoFit = false;
ImGui::Checkbox("Auto-fit", &autoFit);
if(ImPlot::BeginPlot("Frame Times", ImVec2(-1, -1), ImPlotFlags_NoInputs))
{
const int axisFlags = 0; // ImPlotAxisFlags_NoTickLabels
const int axisFlagsY = axisFlags | (autoFit ? ImPlotAxisFlags_AutoFit : 0);
ImPlot::SetupAxes(NULL, NULL, axisFlags, axisFlagsY);
ImPlot::SetupAxisLimits(ImAxis_X1, 0, FrameStats::MaxFrames, ImGuiCond_Always);
if(!autoFit)
{
ImPlot::SetupAxisLimits(ImAxis_Y1, max(target - 2.0, 0.0), target + 2.0, ImGuiCond_Always);
}
ImPlot::SetNextFillStyle(IMPLOT_AUTO_COL, 1.0f);
ImPlot::SetNextLineStyle(IMPLOT_AUTO_COL, 1.0f);
ImPlot::PlotInfLines("Target", &target, 1, ImPlotInfLinesFlags_Horizontal);
ImPlot::SetNextFillStyle(IMPLOT_AUTO_COL, 1.0f);
ImPlot::SetNextLineStyle(IMPLOT_AUTO_COL, 1.0f);
ImPlot::PlotLineG("Frame Time", &FrameTimeGetter, NULL, fs.frameCount, ImPlotLineFlags_None);
ImPlot::EndPlot();
}
}
ImGui::End();
}
GUI_DrawMainMenu();
R_DrawGUI();
}
void SRP::BeginFrame()
{
srp.renderPasses[tr.frameCount % FrameCount].count = 0;
R_SetColorMappings();
// nothing is bound to the command list yet!
srp.renderMode = RenderMode::None;
}
void SRP::EndFrame()
{
tr.tracedWorldShaderIndex = -1;
if(tr.traceWorldShader && tr.world != NULL)
{
CmdBeginBarrier();
CmdBufferBarrier(traceRenderBuffer, ResourceStates::CopySourceBit);
CmdEndBarrier();
// schedule a GPU -> CPU transfer
CmdCopyBuffer(traceReadbackBuffer, traceRenderBuffer);
CmdBeginBarrier();
CmdBufferBarrier(traceRenderBuffer, ResourceStates::UnorderedAccessBit);
CmdEndBarrier();
// grab the currently available result
uint32_t* const shaderIndices = (uint32_t*)MapBuffer(traceReadbackBuffer);
const uint32_t shaderIndex = *shaderIndices;
UnmapBuffer(traceReadbackBuffer);
if(shaderIndex < (uint32_t)tr.numShaders)
{
tr.tracedWorldShaderIndex = (int)shaderIndex;
}
}
RHI::EndFrame();
if(rhie.presentToPresentUS > 0)
{
frameStats.p2pMS[frameStats.frameIndex] = (float)rhie.presentToPresentUS / 1000.0f;
frameStats.EndFrame();
}
else
{
frameStats.skippedFrames++;
}
if(backEnd.renderFrame)
{
Sys_V_EndFrame();
}
}
void SRP::CreateShaderTraceBuffers()
{
{
BufferDesc desc("shader trace render", sizeof(uint32_t), ResourceStates::UnorderedAccessBit);
traceRenderBuffer = CreateBuffer(desc);
}
{
BufferDesc desc("shader trace readback", sizeof(uint32_t), ResourceStates::Common);
desc.memoryUsage = MemoryUsage::Readback;
traceReadbackBuffer = CreateBuffer(desc);
}
}