Files
EVEngine/test/gpgpu.cpp
T
西风逍遥游 34d1fdb63b refactor(modules): clear remaining back-edges and split the largest TUs (#127)
* chore(ci): add clang-format gate, CODEOWNERS and agent collaboration rules

* refactor(modules): clear remaining back-edges and split the largest TUs

Three known dependency back-edges are gone (module_depgraph.py KNOWN_BACK_EDGES
is now empty; --check passes with 0 violations, 0 cycles):

- window -> graphics: window queries the new IWindowSurfaceHost capability
  (common/WindowSurfaceHost.h) instead of holding a Graphics pointer;
  Window::setGraphics was removed and all call sites updated.
- window -> image: Window::setIcon was replaced by setIconRGBA(const uint8_t*,
  w, h); the window no longer decodes ImageData.
- scene -> graphics: pickScreenAt / collectFrustumIdsAt implementations moved
  to graphics/ScenePicking.cpp (graphics -> scene is a legal downward edge);
  scene now depends only on spatial.

TU splits (pure moves, verified identical method sets and brace balance):

- vulkan/Graphics.cpp (5118 lines) -> Graphics.cpp (lifecycle), GraphicsPipeline,
  Graphics2D, Graphics3D, GraphicsMesh + shared GraphicsInternal.h.
- scene/Scene.cpp -> Scene.cpp (script layer / host lifecycle / serialization)
  + SceneNodes.cpp + shared SceneInternal.h.

Verified with a real build (MSVC 19.51, prebuilt third-party): full build green;
window.* and Scene.* suites pass, including scene picking/culling and GPU cases.

* refactor(graphics): narrow Graphics interfaces and clear public-header leaks

Step 4 — interface segregation:
- New lean interfaces: IGraphics2D, IGraphics3D, IResourceFactory, IPostFX
  (pure virtuals, forward-declared types only; graphics/Color.h extracted from
  Canvas.h so modules that only need the color type stop pulling Canvas.h).
- Graphics now implements all four facets; consumers can depend on the narrow
  interface they use instead of the 129-method god class.
- Migrated voxel (VoxelWorld::drawVisible -> IGraphics3D*) and model3d
  (ModelRenderer::buildRenderable(s) -> IResourceFactory&).
- Graphics.h slims from 24 sub-header includes to a handful of small type
  headers + forward declarations; the destructor moved out-of-line so
  unique_ptr effect members are destroyed where types are complete.

Step 5 — header leaks (module_depgraph.py '*' marks: 14 -> 7):
- map/TileLayer.h, particles/ParticleEmitter.h, avatar/AvatarInstance.h,
  animation/SpineAnim.h, map/TileSystem.h, model3d/ModelData.h,
  model3d/ModelRenderer.h, audio/Audio.h + Source.h, image/ImageData.h +
  Image.h, dialogue/Dialogue.h, procgen Palette/OutputSpec/TextureRecipe/
  PbrMaterial -> forward declarations, includes moved to .cpp.
- voxel/VoxelWorld.h: terrain sampler Pimpl'd (unique_ptr + out-of-line
  destructor), removing the procgen header dependency.
- gpgpu/vulkan backend headers stop pulling graphics/vulkan/Graphics.h.
- Remaining '*' marks are deliberate: tiny POD/enum headers (GridConfig,
  MapObject, BlendMode, Color), an internal backend header
  (GraphicsInternal.h -> filesystem), and lean interface references.

Verified with a full clean build (MSVC) and tests: window/Scene/voxel/
RenderSystem/Quad/shader/TextureSampler/resourceLifetime pass; broad suite
only shows the pre-existing PHYSFS-init filesystem-environment failures.

* feat(resources): revive ResourceManager as unified IAssetReloader-backed cache

* fix(resources): leak ResourceManager singleton to avoid exit-time teardown crashes

Cached CPU resources may own third-party handles (FreeType faces, Assimp
scenes, image decode handlers) whose libraries are torn down at process
exit in an unspecified TU order; destroying cached entries from the
singleton destructor can segfault at exit (seen on CI Windows in
model3d.materialApi.objMtl). Keep the singleton alive until OS reclaim;
explicit unload()/clear() still release entries during the run.

* refactor(devtools): consume business modules through capability interfaces

DevTools / cmdline no longer include business-module headers, so trimming
scene / physics / procgen / particles / audio / ui / graphics no longer
requires disabling devtools, and devtools can be switched off with
-DEVENGINE_MODULE_DEVTOOLS=OFF.

New capability interfaces in common/:
- ISceneQuery (scene) - host/node inspection + mutation + transform sync
- IRenderCapture (graphics) - render status, PNG capture, camera control,
  visible-entity snapshot, entity-ID mask, G-buffer PNGs
- IPhysicsQuery (physics) - 2D world lifecycle + ray casts
- IProcgenQuery (procgen) - recipe listing, map/mesh generation
- IParticlesQuery (particles) - emitter status + one-shot emit
- IAudioQuery (audio) - volume / stop-all
- IEditorHost (ui) - declarative editor host (used by cmdline too)

Providers register in their module constructors. DevTools rewires McpServer
(render/scene/procgen/physics/particles/audio/host tools), SceneInspect
(now delegates to ISceneQuery + IRenderCapture) and RenderVision (uses
IRenderCapture::capturePngDataUrl); cmdline/Mcp.cpp uses IEditorHost.

manifest: devtools DEPS graphics scene physics procgen particles audio ui
image event filesystem -> event; cmdline DEPS filesystem ui -> filesystem.

Verified: full build green; devtools.mcp.* tests pass; broad suite shows
only the pre-existing PHYSFS-init filesystem-environment failures.

* style: apply clang-format to changed lines after dev merge

* fix(webgpu): include RenderControl/TextureSampler explicitly after Graphics.h slimming

* fix(webgpu): provide newFont/print stubs to satisfy IGraphics2D/IResourceFactory vtables

* fix(common): make ProcgenQuery.h self-sufficient (cstdint/utility for GCC)
2026-08-20 05:53:33 +08:00

361 lines
10 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "common/ECS.h"
#include "gpgpu/ComputeShader.h"
#include "gpgpu/EcsGpu.h"
#include "gpgpu/Gpgpu.h"
#include "gpgpu/GpuBuffer.h"
#include "gpgpu/Sequence.h"
#include "gpgpu/ShaderSystem.h"
#include "graphics/AmbientOcclusion.h"
#include "graphics/AntiAliasing.h"
#include "graphics/Canvas.h"
#include "graphics/DrawItem2D.h"
#include "graphics/Font.h"
#include "graphics/GBuffer.h"
#include "graphics/GlobalIllumination.h"
#include "graphics/Graphics.h"
#include "graphics/Grass.h"
#include "graphics/Light.h"
#include "graphics/Material.h"
#include "graphics/Mesh.h"
#include "graphics/Outline.h"
#include "graphics/Quad.h"
#include "graphics/RenderControl.h"
#include "graphics/ScreenSpaceReflection.h"
#include "graphics/Shader.h"
#include "graphics/Texture.h"
#include "graphics/Volumetric.h"
#include "graphics/Water.h"
#include "graphics/Waterfall.h"
#include "window/Window.h"
#include <cmath>
#include <string>
#include <vector>
using namespace eve::gpgpu;
namespace {
bool tryInitGpuWindow() {
auto *win = eve::window::Window::create();
auto *gfx = eve::graphics::Graphics::create();
if (!win || !gfx) return false;
eve::window::WindowSettings s;
s.width = 320;
s.height = 240;
s.centered = true;
return win->setWindowSettings(s);
}
const char *kScaleKernel = R"(#version 450
layout(local_size_x = 64) in;
layout(set = 0, binding = 0) buffer Data {
float values[];
} data;
layout(push_constant) uniform PC {
float data[32];
} pc;
void main() {
uint i = gl_GlobalInvocationID.x;
data.values[i] *= pc.data[0];
}
)";
const char *kMoveKernel = R"(#version 450
layout(local_size_x = 64) in;
layout(set = 0, binding = 0) buffer Pos {
float data[];
} pos;
layout(set = 0, binding = 1) buffer Vel {
float data[];
} vel;
layout(push_constant) uniform PC {
float data[32];
} pc;
void main() {
uint i = gl_GlobalInvocationID.x;
uint n = uint(pc.data[1]);
if (i >= n) return;
float dt = pc.data[0];
uint b = i * 2u;
pos.data[b + 0u] += vel.data[b + 0u] * dt;
pos.data[b + 1u] += vel.data[b + 1u] * dt;
}
)";
const char *kDoubleKernel = R"(#version 450
layout(local_size_x = 64) in;
layout(set = 0, binding = 0) buffer In {
float a[];
} inBuf;
layout(set = 0, binding = 1) buffer Out {
float b[];
} outBuf;
void main() {
uint i = gl_GlobalInvocationID.x;
outBuf.b[i] = inBuf.a[i] * 2.0;
}
)";
class GpuNode : public ecs::Entity {
public:
ENTITY(GpuNode, ecs::Entity)
void release() override { ecs::DestroyEntity(this); }
struct Position {
float x = 0;
float y = 0;
};
COMPONENT(Position, position)
struct Velocity {
float dx = 0;
float dy = 0;
};
COMPONENT(Velocity, velocity)
};
} // namespace
TEST_CASE("gpgpu.module.create") {
auto *mod = Gpgpu::create();
REQUIRE(mod != nullptr);
CHECK_EQ(mod->getName(), std::string("Gpgpu"));
}
TEST_CASE("gpgpu.graphics.backendName") {
if (!tryInitGpuWindow()) return;
auto *gfx = eve::graphics::Graphics::create();
REQUIRE(gfx != nullptr);
CHECK_EQ(gfx->getBackendName(), std::string("vulkan"));
}
TEST_CASE("gpgpu.newShaderFromSpvFile.delegatesWhenMissing") {
if (!tryInitGpuWindow()) return;
auto *mod = Gpgpu::create();
if (!mod->isAvailable()) return;
bool threw = false;
try {
mod->newShaderFromSpvFile("__eve_missing_compute.spv");
} catch (...) {
threw = true;
}
CHECK(threw);
}
TEST_CASE("gpgpu.dispatch.scaleFloats") {
if (!tryInitGpuWindow()) return;
auto *mod = Gpgpu::create();
REQUIRE(mod->isAvailable());
const int count = 128;
GpuBuffer *buf = mod->newBuffer(count * int(sizeof(float)), "storage");
REQUIRE(buf != nullptr);
CHECK_EQ(buf->getSize(), count * int(sizeof(float)));
for (int i = 0; i < count; ++i)
buf->writeFloat32(i, float(i + 1));
ComputeShader *shader = nullptr;
try {
shader = mod->newShader(kScaleKernel);
} catch (...) {
// glslc may be missing in some CI images — skip GPU path.
return;
}
REQUIRE(shader != nullptr);
shader->bindBuffer(0, buf);
shader->setFloat(0, 2.f);
const int groups = (count + 63) / 64;
mod->dispatch(shader, groups, 1, 1);
CHECK(std::fabs(buf->readFloat32(0) - 2.f) < 1e-4f);
CHECK(std::fabs(buf->readFloat32(1) - 4.f) < 1e-4f);
CHECK(std::fabs(buf->readFloat32(count - 1) - float(count) * 2.f) < 1e-3f);
delete shader;
delete buf;
}
TEST_CASE("gpgpu.sequence.dispatchScale") {
if (!tryInitGpuWindow()) return;
auto *mod = Gpgpu::create();
REQUIRE(mod->isAvailable());
const int count = 128;
GpuBuffer *in = mod->newBuffer(count * int(sizeof(float)), "storage");
GpuBuffer *out = mod->newBuffer(count * int(sizeof(float)), "storage");
GpuBuffer *staging = mod->newBuffer(count * int(sizeof(float)), "staging");
REQUIRE(in != nullptr);
REQUIRE(out != nullptr);
REQUIRE(staging != nullptr);
ComputeShader *shader = nullptr;
try {
shader = mod->newShader(kDoubleKernel);
} catch (...) {
delete in;
delete out;
delete staging;
return; // no compiler available — skip GPU path
}
REQUIRE(shader != nullptr);
std::vector<float> src;
src.reserve(count);
for (int i = 0; i < count; ++i) src.push_back(float(i + 1));
Sequence *seq = mod->newSequence();
REQUIRE(seq->isAvailable());
shader->bindBuffer(0, in);
shader->bindBuffer(1, out);
const int groups = (count + 63) / 64;
seq->begin();
seq->recordUpload(in, src.data(), uint64_t(count) * sizeof(float));
seq->recordDispatch(shader, groups, 1, 1);
seq->recordDownload(out, staging, uint64_t(count) * sizeof(float));
seq->submit();
std::vector<float> dst(size_t(count), 0.f);
staging->downloadBytes(dst.data(), uint64_t(count) * sizeof(float));
CHECK(std::fabs(dst[0] - 2.f) < 1e-4f);
CHECK(std::fabs(dst[1] - 4.f) < 1e-4f);
CHECK(std::fabs(dst[count - 1] - float(count) * 2.f) < 1e-3f);
delete seq;
delete shader;
delete in;
delete out;
delete staging;
}
TEST_CASE("gpgpu.sequence.singleSubmitChainedDispatches") {
if (!tryInitGpuWindow()) return;
auto *mod = Gpgpu::create();
REQUIRE(mod->isAvailable());
const int count = 256;
GpuBuffer *a = mod->newBuffer(count * int(sizeof(float)), "storage");
GpuBuffer *b = mod->newBuffer(count * int(sizeof(float)), "storage");
GpuBuffer *c = mod->newBuffer(count * int(sizeof(float)), "storage");
GpuBuffer *staging = mod->newBuffer(count * int(sizeof(float)), "staging");
ComputeShader *shader = nullptr;
try {
shader = mod->newShader(kDoubleKernel);
} catch (...) {
delete a;
delete b;
delete c;
delete staging;
return;
}
std::vector<float> src(static_cast<size_t>(count));
for (int i = 0; i < count; ++i) src[size_t(i)] = float(i % 7);
Sequence *seq = mod->newSequence();
REQUIRE(seq->isAvailable());
const int groups = (count + 63) / 64;
// a -> b -> c in one submission; c[i] must be src[i] * 4.
seq->begin();
seq->recordUpload(a, src.data(), uint64_t(count) * sizeof(float));
shader->bindBuffer(0, a);
shader->bindBuffer(1, b);
seq->recordDispatch(shader, groups, 1, 1);
shader->bindBuffer(0, b);
shader->bindBuffer(1, c);
seq->recordDispatch(shader, groups, 1, 1);
seq->recordDownload(c, staging, uint64_t(count) * sizeof(float));
seq->submit();
std::vector<float> dst(size_t(count), 0.f);
staging->downloadBytes(dst.data(), uint64_t(count) * sizeof(float));
for (int i = 0; i < count; ++i)
CHECK(std::fabs(dst[size_t(i)] - float(i % 7) * 4.f) < 1e-3f);
// Reuse the same sequence for a second cycle.
seq->begin();
seq->recordUpload(a, src.data(), uint64_t(count) * sizeof(float));
shader->bindBuffer(0, a);
shader->bindBuffer(1, b);
seq->recordDispatch(shader, groups, 1, 1);
seq->recordDownload(b, staging, uint64_t(count) * sizeof(float));
seq->submit();
staging->downloadBytes(dst.data(), uint64_t(count) * sizeof(float));
CHECK(std::fabs(dst[0] - 0.f) < 1e-4f);
CHECK(std::fabs(dst[count - 1] - float((count - 1) % 7) * 2.f) < 1e-3f);
delete seq;
delete shader;
delete a;
delete b;
delete c;
delete staging;
}
TEST_CASE("gpgpu.shaderSystem.ecsMove") {
if (!tryInitGpuWindow()) return;
auto *mod = Gpgpu::create();
REQUIRE(mod->isAvailable());
ecs::Table world;
ecs::ScopedTable guard(world);
auto *a = GpuNode::create();
a->position()->x = 1.f;
a->position()->y = 2.f;
a->velocity()->dx = 10.f;
a->velocity()->dy = 20.f;
auto *b = GpuNode::create();
b->position()->x = 0.f;
b->position()->y = 0.f;
b->velocity()->dx = -5.f;
b->velocity()->dy = 1.f;
ShaderSystem sys;
sys.setGpgpu(mod);
try {
sys.setShaderSource(kMoveKernel);
} catch (...) {
return;
}
const int nPos = packViewComponent<GpuNode, GpuNode::Position>(sys, 0);
const int nVel = packViewComponent<GpuNode, GpuNode::Velocity>(sys, 1);
REQUIRE_EQ(nPos, nVel);
REQUIRE_GE(nPos, 2);
sys.dispatch(nPos, 0.5f);
unpackViewComponent<GpuNode, GpuNode::Position>(sys, 0, nPos);
CHECK(std::fabs(a->position()->x - 6.f) < 1e-3f);
CHECK(std::fabs(a->position()->y - 12.f) < 1e-3f);
CHECK(std::fabs(b->position()->x - (-2.5f)) < 1e-3f);
CHECK(std::fabs(b->position()->y - 0.5f) < 1e-3f);
}
TEST_CASE("gpgpu.buffer.bulkFloats") {
if (!tryInitGpuWindow()) return;
auto *mod = Gpgpu::create();
REQUIRE(mod->isAvailable());
std::vector<float> src = {1.f, 2.f, 3.f, 4.f};
GpuBuffer *buf = mod->newBuffer(int(src.size() * sizeof(float)), "storage");
buf->writeFloat32s(src.data(), int(src.size()), 0);
std::vector<float> dst(src.size(), 0.f);
buf->readFloat32s(dst.data(), int(dst.size()), 0);
CHECK_EQ(dst[0], 1.f);
CHECK_EQ(dst[3], 4.f);
delete buf;
}