Enable M2 particle emitters with correct WotLK struct parsing and overflow guards

This commit is contained in:
Kelsi 2026-02-06 20:57:02 -08:00
parent 12fb879e1a
commit 104a9d0898
2 changed files with 77 additions and 21 deletions

View file

@ -252,12 +252,16 @@ T readValue(const std::vector<uint8_t>& data, uint32_t offset) {
template<typename T>
std::vector<T> readArray(const std::vector<uint8_t>& data, uint32_t offset, uint32_t count) {
std::vector<T> result;
if (count == 0 || offset + count * sizeof(T) > data.size()) {
return result;
}
if (count == 0) return result;
// Overflow-safe bounds check: avoid uint32 wrap on count * sizeof(T)
size_t totalBytes = static_cast<size_t>(count) * sizeof(T);
if (totalBytes / sizeof(T) != count) return result; // multiplication overflowed
if (static_cast<size_t>(offset) + totalBytes > data.size()) return result;
// Sanity cap: refuse allocations > 64MB to prevent garbage counts from OOMing
if (totalBytes > 64 * 1024 * 1024) return result;
result.resize(count);
std::memcpy(result.data(), &data[offset], count * sizeof(T));
std::memcpy(result.data(), &data[offset], totalBytes);
return result;
}
@ -652,17 +656,61 @@ M2Model M2Loader::load(const std::vector<uint8_t>& m2Data) {
model.attachmentLookup = readArray<uint16_t>(m2Data, header.ofsAttachmentLookup, header.nAttachmentLookup);
}
// Particle emitter parsing disabled.
// The assumed EMITTER_STRUCT_SIZE (476 bytes) is incorrect for WotLK 3.3.5a M2 files.
// When iterating multiple emitters, each one after the first reads from a misaligned
// offset, producing garbage M2TrackDisk headers with huge nTimestamps/nKeys counts.
// parseAnimTrack then calls readArray which allocates vectors sized by those garbage
// counts — this caused RAM usage to explode from ~1 GB to 130+ GB, consuming all
// system memory and swap.
// TODO: determine the correct emitter struct size for build 12340 and add overflow
// guards to readArray (count * sizeof(T) can wrap uint32_t, bypassing bounds checks).
(void)header.nParticleEmitters;
(void)header.ofsParticleEmitters;
// Parse particle emitters (WotLK M2ParticleOld: 0x1F0 = 496 bytes per emitter)
static constexpr uint32_t EMITTER_STRUCT_SIZE = 0x1F0;
if (header.nParticleEmitters > 0 && header.ofsParticleEmitters > 0 &&
header.nParticleEmitters < 256 &&
static_cast<size_t>(header.ofsParticleEmitters) +
static_cast<size_t>(header.nParticleEmitters) * EMITTER_STRUCT_SIZE <= m2Data.size()) {
// Build sequence flags for parseAnimTrack
std::vector<uint32_t> emSeqFlags;
emSeqFlags.reserve(model.sequences.size());
for (const auto& seq : model.sequences) {
emSeqFlags.push_back(seq.flags);
}
for (uint32_t ei = 0; ei < header.nParticleEmitters; ei++) {
uint32_t base = header.ofsParticleEmitters + ei * EMITTER_STRUCT_SIZE;
M2ParticleEmitter em;
em.particleId = readValue<int32_t>(m2Data, base + 0x00);
em.flags = readValue<uint32_t>(m2Data, base + 0x04);
em.position.x = readValue<float>(m2Data, base + 0x08);
em.position.y = readValue<float>(m2Data, base + 0x0C);
em.position.z = readValue<float>(m2Data, base + 0x10);
em.bone = readValue<uint16_t>(m2Data, base + 0x14);
em.texture = readValue<uint16_t>(m2Data, base + 0x16);
em.blendingType = readValue<uint8_t>(m2Data, base + 0x28);
em.emitterType = readValue<uint8_t>(m2Data, base + 0x29);
// Parse animated tracks (M2TrackDisk at known offsets)
auto parseTrack = [&](uint32_t off, M2AnimationTrack& track) {
if (base + off + sizeof(M2TrackDisk) <= m2Data.size()) {
M2TrackDisk disk = readValue<M2TrackDisk>(m2Data, base + off);
parseAnimTrack(m2Data, disk, track, TrackType::FLOAT, emSeqFlags);
}
};
parseTrack(0x34, em.emissionSpeed);
parseTrack(0x48, em.speedVariation);
parseTrack(0x5C, em.verticalRange);
parseTrack(0x70, em.horizontalRange);
parseTrack(0x84, em.gravity);
parseTrack(0x98, em.lifespan);
parseTrack(0xB0, em.emissionRate);
parseTrack(0xC8, em.emissionAreaLength);
parseTrack(0xDC, em.emissionAreaWidth);
parseTrack(0xF0, em.deceleration);
// Parse FBlocks (color, alpha, scale)
parseFBlock(m2Data, base + 0x104, em.particleColor, 0);
parseFBlock(m2Data, base + 0x118, em.particleAlpha, 1);
parseFBlock(m2Data, base + 0x12C, em.particleScale, 2);
model.particleEmitters.push_back(std::move(em));
}
core::Logger::getInstance().debug(" Particle emitters: ", model.particleEmitters.size());
}
static int m2LoadLogBudget = 200;
if (m2LoadLogBudget-- > 0) {

View file

@ -824,7 +824,15 @@ bool M2Renderer::loadModel(const pipeline::M2Model& model, uint32_t modelId) {
}
}
// Particle emitter data copy disabled (parsing disabled for now)
// Copy particle emitter data and resolve textures
gpuModel.particleEmitters = model.particleEmitters;
gpuModel.particleTextures.resize(model.particleEmitters.size(), whiteTexture);
for (size_t ei = 0; ei < model.particleEmitters.size(); ei++) {
uint16_t texIdx = model.particleEmitters[ei].texture;
if (texIdx < allTextures.size() && allTextures[texIdx] != 0) {
gpuModel.particleTextures[ei] = allTextures[texIdx];
}
}
// Copy texture transform data for UV animation
gpuModel.textureTransforms = model.textureTransforms;
@ -1261,11 +1269,11 @@ void M2Renderer::update(float deltaTime) {
computeBoneMatrices(model, instance);
// M2 particle emitter update — disabled for now (too expensive with many instances)
// if (!model.particleEmitters.empty()) {
// emitParticles(instance, model, deltaTime);
// updateParticles(instance, deltaTime);
// }
// M2 particle emitter update
if (!model.particleEmitters.empty()) {
emitParticles(instance, model, deltaTime);
updateParticles(instance, deltaTime);
}
}
}