Files
EVEngine/test/gpu_driven.cpp
T
西风逍遥游 d72b292d47 fix(graphics): GPU-driven shading parity + AMD driver workarounds
End-to-end parity testing (gpuDriven on/off pixel comparison) exposed and
fixed several shading-path bugs, and documented two hard AMD driver
limitations on this machine (Radeon AI PRO R9700):

1. Missing ambient in the GPU-driven frame UBO: the per-frame UBO only set
   ambient.w; ambient.rgb came from stale state. Now mirrors the legacy
   draw path (mesh3dLighting.ambient).
2. Normal-map fallback: materials without a normal texture clamped to the
   bindless slot 0, which is the WHITE placeholder - the legacy path uses a
   (128,128,255) flat-normal texture. The fragment shader now skips
   applyNormalMap when the material has no normal texture.
3. AMD driver limitation A: vkUpdateDescriptorSets on the sampler array
   binding hangs for writes of >= 4 elements; placeholder fill uses
   single-element writes (~1.1k calls at init, ~3 ms).
4. AMD driver limitation B: descriptor-array dynamic indexing only returns
   correct results for element 0. Scenes with more than one distinct
   texture therefore fall back to the legacy path via the new
   gpuDrivenMaterialUsable() gate (RenderSystem3D eligibility check). The
   proper fix (texture-array/atlas with layer indexing) is Phase 3 work.

Adds GpuDriven.opaqueForwardParity: renders the same single-texture scene
with the legacy and GPU-driven paths and asserts per-pixel luma agreement
(max delta < 0.03). Verified passing in a clean process; the AMD driver
wedges intermittently under repeated test runs on this machine, which is
tracked separately.

The gpuDriven feature remains opt-in (RenderControl, default OFF), so
default rendering is unchanged.
2026-08-21 11:21:02 +08:00

192 lines
5.9 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include <SDL2/SDL.h>
#include "graphics/Graphics.h"
#include "graphics/Light.h"
#include "graphics/Material.h"
#include "graphics/Mesh.h"
#include "graphics/RenderControl.h"
#include "graphics/RenderSystem.h"
#include "graphics/RenderSystem3D.h"
#include "graphics/Texture.h"
#include "graphics/vulkan/Graphics.h"
#include "graphics/vulkan/GpuDriven.h"
#include "window/Window.h"
#include <algorithm>
#include <cmath>
#include <vector>
using namespace eve::graphics;
namespace {
void openGfxWindow(eve::window::Window *&win, Graphics *&gfx, int w = 320, int h = 240) {
win = eve::window::Window::create();
gfx = Graphics::create();
REQUIRE(win != nullptr);
REQUIRE(gfx != nullptr);
win->setGraphics(gfx);
eve::window::WindowSettings s;
s.width = w;
s.height = h;
s.centered = true;
REQUIRE(win->setWindowSettings(s));
}
} // namespace
TEST_CASE("GpuDriven.capsAvailable") {
eve::window::Window *win = nullptr;
Graphics *gfx = nullptr;
openGfxWindow(win, gfx);
auto *vg = dynamic_cast<eve::graphics::vulkan::Graphics *>(gfx);
REQUIRE(vg != nullptr);
CHECK(vg->gpuDrivenCaps().gpuDrivenAvailable());
win->close();
}
TEST_CASE("GpuDriven.bindlessTextureRegistration") {
eve::window::Window *win = nullptr;
Graphics *gfx = nullptr;
openGfxWindow(win, gfx);
auto *vg = dynamic_cast<eve::graphics::vulkan::Graphics *>(gfx);
REQUIRE(vg != nullptr);
if (!vg->gpuDrivenCaps().gpuDrivenAvailable()) {
win->close();
return; // environment without 1.2 features: nothing to validate
}
const uint8_t red[4] = {255, 0, 0, 255};
const uint8_t green[4] = {0, 255, 0, 255};
Texture *a = gfx->newTexture(1, 1, red);
Texture *b = gfx->newTexture(1, 1, green);
const uint32_t sa = vg->debugBindlessIndex(a);
const uint32_t sb = vg->debugBindlessIndex(b);
CHECK(sa != eve::graphics::vulkan::kInvalidBindlessSlot);
CHECK(sb != eve::graphics::vulkan::kInvalidBindlessSlot);
CHECK(sa != sb); // distinct slots
const std::vector<uint8_t> cubePx(6 * 4, 128);
Texture *cube = gfx->newCubemap(1, cubePx.data());
auto *gpuCube = static_cast<eve::graphics::vulkan::GpuTexture *>(cube->gpuHandle);
CHECK(gpuCube->bindlessIndexCube != eve::graphics::vulkan::kInvalidBindlessSlot);
win->close();
}
TEST_CASE("GpuDriven.meshTableRegistration") {
eve::window::Window *win = nullptr;
Graphics *gfx = nullptr;
openGfxWindow(win, gfx);
auto *vg = dynamic_cast<eve::graphics::vulkan::Graphics *>(gfx);
REQUIRE(vg != nullptr);
if (!vg->gpuDrivenCaps().gpuDrivenAvailable()) {
win->close();
return;
}
Mesh *m = gfx->newMeshSphere(8, 4);
REQUIRE(m != nullptr);
const uint32_t idx = vg->debugMeshRecordIndex(m);
CHECK(idx != eve::graphics::vulkan::kInvalidBindlessSlot);
auto *gpu = static_cast<eve::graphics::vulkan::GpuMesh *>(m->gpuHandle);
CHECK(gpu->record.vertexCount > 0);
CHECK(gpu->record.indexCount > 0);
CHECK(gpu->record.boundsCenterRadius.w > 0.f); // bounds computed at upload
win->close();
}
namespace {
float gdLuma(const Color &c) { return (c.r + c.g + c.b) / 3.f; }
void gdWarmPresent(Graphics *gfx) {
for (int i = 0; i < 4; ++i) {
RenderSystem3D::render(*gfx);
RenderSystem::render(*gfx);
}
}
std::vector<float> gdCaptureLuma(Graphics *gfx) {
const int w = gfx->getWidth();
const int h = gfx->getHeight();
std::vector<float> out(size_t(w) * size_t(h), 0.f);
for (int y = 0; y < h; y += 4) {
for (int x = 0; x < w; x += 4) {
out[size_t(y * w + x)] = gdLuma(gfx->getPixel(x, y));
}
}
return out;
}
Texture *gdSolid(Graphics *gfx, uint8_t r, uint8_t g, uint8_t b) {
const uint8_t px[4] = {r, g, b, 255};
return gfx->newTexture(1, 1, px);
}
} // namespace
/**
* @brief The opaque forward pass must produce the same image with the legacy
* per-draw path and the GPU-driven (indirect + bindless) path.
*/
TEST_CASE("GpuDriven.opaqueForwardParity") {
eve::window::Window *win = nullptr;
Graphics *gfx = nullptr;
openGfxWindow(win, gfx, 320, 240);
auto *vg = dynamic_cast<eve::graphics::vulkan::Graphics *>(gfx);
REQUIRE(vg != nullptr);
if (!vg->gpuDrivenCaps().gpuDrivenAvailable()) {
win->close();
return;
}
auto *cam = Camera3D::createCamera();
cam->setEye(0.f, 3.5f, 5.5f);
cam->setTarget(0.f, 0.f, 0.f);
cam->setAmbient(0.08f, 0.08f, 0.10f);
Material *ballMat = gfx->newMaterial();
ballMat->setAlbedoTexture(gdSolid(gfx, 205, 70, 60)); // single texture -> slot 0
ballMat->setNormalTexture(nullptr);
ballMat->setRoughness(0.5f);
ballMat->setMetallic(0.1f);
auto *ball = Renderable3D::create();
ball->setMesh(gfx->newMeshSphere(24, 16));
ball->setMaterial(ballMat);
ball->setPosition(0.f, 0.35f, 0.f);
ball->setScale(0.55f, 0.55f, 0.55f);
auto *sun = Light3D::createLight("dir");
sun->setDirection(0.55f, 1.f, 0.35f);
sun->setColor(1.f, 1.f, 1.f, 2.5f);
sun->setCastShadow(false); // isolate the direct-light path first
gfx->setScreenReadbackEnabled(true);
RenderControl *rc = gfx->getRenderControl();
rc->disable("gpuDriven");
gdWarmPresent(gfx);
const auto legacy = gdCaptureLuma(gfx);
rc->enable("gpuDriven");
gdWarmPresent(gfx);
const auto gpuDriven = gdCaptureLuma(gfx);
// Same shading source, different emission path: allow small float noise.
float maxDelta = 0.f;
const int w = gfx->getWidth();
const int h = gfx->getHeight();
for (int y = 0; y < h; y += 4) {
for (int x = 0; x < w; x += 4) {
const size_t i = size_t(y * w + x);
const float d = std::fabs(legacy[i] - gpuDriven[i]);
maxDelta = std::max(maxDelta, d);
}
}
rc->disable("gpuDriven");
CHECK(maxDelta < 0.03f);
win->close();
}