Files
西风逍遥游 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

291 lines
9.0 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "common/Exception.h"
#include "data/ByteData.h"
#include "filesystem/FileData.h"
#include "sound/Decoder.h"
#include "sound/Sound.h"
#include "sound/SoundData.h"
#include <cstdlib>
#include <cstring>
#include <cstdint>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
namespace {
#include "PathBesideSource.h"
EVE_DEFINE_PATH_BESIDE_SOURCE()
std::vector<char> readBinaryFile(const std::string &path) {
std::ifstream in(path, std::ios::binary);
if (!in)
return {};
return std::vector<char>((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
}
} // namespace
static std::vector<char> makeSilentWav(int samples = 441) {
const int dataBytes = samples * 2;
std::vector<char> buf(44 + dataBytes, 0);
auto w32 = [&](int off, uint32_t v) { std::memcpy(buf.data() + off, &v, 4); };
auto w16 = [&](int off, uint16_t v) { std::memcpy(buf.data() + off, &v, 2); };
std::memcpy(buf.data(), "RIFF", 4);
w32(4, 36 + dataBytes);
std::memcpy(buf.data() + 8, "WAVEfmt ", 8);
w32(16, 16);
w16(20, 1);
w16(22, 1);
w32(24, 44100);
w32(28, 44100 * 2);
w16(32, 2);
w16(34, 16);
std::memcpy(buf.data() + 36, "data", 4);
w32(40, dataBytes);
return buf;
}
TEST_CASE("sound.newDecoder.wav") {
auto *sound = eve::sound::Sound::create();
auto wav = makeSilentWav();
eve::data::ByteData data(wav.data(), wav.size());
auto *dec = sound->newDecoder(&data);
REQUIRE(dec != nullptr);
CHECK(dec->getSampleRate() == 44100);
CHECK(dec->getChannelCount() == 1);
CHECK(dec->getBitDepth() == 16);
delete dec;
}
TEST_CASE("sound.newSoundData.wav") {
auto *sound = eve::sound::Sound::create();
auto wav = makeSilentWav(4410);
eve::data::ByteData data(wav.data(), wav.size());
auto *sd = sound->newSoundData(&data);
REQUIRE(sd != nullptr);
CHECK(sd->getSampleRate() == 44100);
CHECK(sd->getChannelCount() == 1);
CHECK(sd->getBitDepth() == 16);
CHECK(sd->getSampleCount() == 4410);
CHECK(sd->getDuration() > 0.09);
CHECK(sd->getSize() == 4410 * 2);
delete sd;
}
TEST_CASE("sound.newSoundDataEmpty") {
auto *sound = eve::sound::Sound::create();
auto *sd = sound->newSoundDataEmpty(100, 22050, 16, 2);
REQUIRE(sd != nullptr);
CHECK(sd->getSampleCount() == 100);
CHECK(sd->getChannelCount() == 2);
CHECK(sd->getSize() == 100 * 2 * 2);
delete sd;
}
TEST_CASE("sound.decoder.seekRewindIsFinished") {
auto *sound = eve::sound::Sound::create();
auto wav = makeSilentWav(44100);
eve::data::ByteData data(wav.data(), wav.size());
auto *dec = sound->newDecoder(&data);
REQUIRE(dec != nullptr);
CHECK(dec->isSeekable());
CHECK(!dec->isFinished());
CHECK(dec->getDuration() > 0.9);
CHECK(dec->decode() > 0);
CHECK(dec->seek(0.5));
CHECK(!dec->isFinished());
while (!dec->isFinished()) {
if (dec->decode() <= 0)
break;
}
CHECK(dec->isFinished());
CHECK(dec->rewind());
CHECK(!dec->isFinished());
CHECK(dec->decode() > 0);
delete dec;
}
TEST_CASE("sound.decoder.clone") {
auto *sound = eve::sound::Sound::create();
auto wav = makeSilentWav(4410);
eve::data::ByteData data(wav.data(), wav.size());
auto *dec = sound->newDecoder(&data);
REQUIRE(dec != nullptr);
auto *cloned = dec->clone();
REQUIRE(cloned != nullptr);
CHECK(cloned->getSampleRate() == dec->getSampleRate());
CHECK(cloned->getChannelCount() == dec->getChannelCount());
CHECK(cloned->getBitDepth() == dec->getBitDepth());
dec->decode();
CHECK(cloned->decode() > 0);
delete cloned;
delete dec;
}
TEST_CASE("sound.newSoundDataFromDecoder") {
auto *sound = eve::sound::Sound::create();
auto wav = makeSilentWav(4410);
eve::data::ByteData data(wav.data(), wav.size());
auto *dec = sound->newDecoder(&data);
REQUIRE(dec != nullptr);
auto *sd = sound->newSoundDataFromDecoder(dec);
REQUIRE(sd != nullptr);
CHECK(sd->getSampleCount() == 4410);
CHECK(sd->getSampleRate() == 44100);
CHECK(sd->getChannelCount() == 1);
CHECK(sd->getBitDepth() == 16);
CHECK(sd->getSize() == 4410 * 2);
delete sd;
delete dec;
}
TEST_CASE("sound.invalidData") {
auto *sound = eve::sound::Sound::create();
try {
sound->newDecoder(nullptr);
CHECK(false);
} catch (const eve::Exception &) {
CHECK(true);
}
try {
const char garbage[] = "not a sound file";
eve::data::ByteData bad(garbage, sizeof(garbage) - 1);
sound->newDecoder(&bad);
CHECK(false);
} catch (const eve::Exception &) {
CHECK(true);
}
try {
sound->newSoundDataFromDecoder(nullptr);
CHECK(false);
} catch (const eve::Exception &) {
CHECK(true);
}
try {
sound->newSoundData(nullptr);
CHECK(false);
} catch (const eve::Exception &) {
CHECK(true);
}
try {
const char garbage[] = "not a sound file either";
eve::data::ByteData bad(garbage, sizeof(garbage) - 1);
sound->newSoundData(&bad);
CHECK(false);
} catch (const eve::Exception &) {
CHECK(true);
}
}
TEST_CASE("sound.modplug.midi.decode") {
const std::string path = pathBesideThisSource("angel.mid");
auto bytes = readBinaryFile(path);
REQUIRE(!bytes.empty());
REQUIRE(bytes.size() >= 4);
CHECK(std::memcmp(bytes.data(), "MThd", 4) == 0);
eve::filesystem::FileData fd("angel.mid", bytes.size());
std::memcpy(fd.getData(), bytes.data(), bytes.size());
auto *sound = eve::sound::Sound::create();
eve::sound::Decoder *dec = nullptr;
try {
dec = sound->newDecoder(&fd);
} catch (const eve::Exception &e) {
std::cerr << "MIDI decode failed: " << e.what() << "\n";
CHECK(false);
return;
}
REQUIRE(dec != nullptr);
CHECK(dec->getSampleRate() > 0);
CHECK(dec->getChannelCount() == 2);
CHECK(dec->getBitDepth() == 16);
CHECK(dec->getDuration() > 0.0);
int decoded = 0;
int64_t absSum = 0;
int samplesChecked = 0;
while (!dec->isFinished()) {
int n = dec->decode();
if (n <= 0)
break;
decoded += n;
auto *buf = static_cast<const int16_t *>(dec->getBuffer());
int samples = n / 2;
for (int i = 0; i < samples && samplesChecked < 44100 * 4; ++i) {
absSum += buf[i] < 0 ? -buf[i] : buf[i];
++samplesChecked;
}
if (samplesChecked >= 44100 * 4)
break;
}
std::cerr << "MIDI decode: duration=" << dec->getDuration()
<< "s bytes=" << decoded << " absSum=" << absSum << "\n";
CHECK(decoded > 0);
// ModPlug may load MIDI but output silence without instrument patches.
if (absSum == 0)
std::cerr << "WARNING: MIDI decoded to silence (likely no instruments)\n";
// Optional: EVENGINE_DUMP_MIDI=1 writes ~3s WAV for manual afplay.
if (const char *dump = std::getenv("EVENGINE_DUMP_MIDI"); dump && dump[0] == '1') {
eve::filesystem::FileData fd2("angel.mid", bytes.size());
std::memcpy(fd2.getData(), bytes.data(), bytes.size());
auto *dec2 = sound->newDecoder(&fd2);
const int rate = dec2->getSampleRate();
const int ch = dec2->getChannelCount();
const int wantBytes = rate * ch * 2 * 3;
std::vector<uint8_t> pcm;
pcm.reserve(static_cast<size_t>(wantBytes));
while ((int)pcm.size() < wantBytes && !dec2->isFinished()) {
int n = dec2->decode();
if (n <= 0)
break;
auto *buf = static_cast<const uint8_t *>(dec2->getBuffer());
pcm.insert(pcm.end(), buf, buf + n);
}
delete dec2;
if (pcm.size() > static_cast<size_t>(wantBytes))
pcm.resize(static_cast<size_t>(wantBytes));
const uint32_t dataBytes = static_cast<uint32_t>(pcm.size());
std::vector<char> wav(44 + pcm.size());
auto w32 = [&](int off, uint32_t v) { std::memcpy(wav.data() + off, &v, 4); };
auto w16 = [&](int off, uint16_t v) { std::memcpy(wav.data() + off, &v, 2); };
std::memcpy(wav.data(), "RIFF", 4);
w32(4, 36 + dataBytes);
std::memcpy(wav.data() + 8, "WAVEfmt ", 8);
w32(16, 16);
w16(20, 1);
w16(22, static_cast<uint16_t>(ch));
w32(24, static_cast<uint32_t>(rate));
w32(28, static_cast<uint32_t>(rate * ch * 2));
w16(32, static_cast<uint16_t>(ch * 2));
w16(34, 16);
std::memcpy(wav.data() + 36, "data", 4);
w32(40, dataBytes);
std::memcpy(wav.data() + 44, pcm.data(), pcm.size());
const std::string out = std::string(EVENGINE_TEST_BINARY_DIR) + "/angel_modplug.wav";
std::ofstream ofs(out, std::ios::binary);
ofs.write(wav.data(), static_cast<std::streamsize>(wav.size()));
std::cerr << "Wrote " << out << " (" << pcm.size() << " pcm bytes)\n";
}
delete dec;
}