* 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)
291 lines
9.0 KiB
C++
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;
|
|
}
|