Pick geometry completeness + the audio crackle root cause

Device raycast/scene parser (vpxlog.cpp):
- Connection lists: accumulate the WHOLE batch before slicing (records
  straddling burst boundaries were dropped + misaligned the rest), do
  not wipe polys per batch header (floor ships tri batch + quad batch;
  only the last survived), keep every loop index (closing-dup drop
  halved each quad). Together these were the "holey ground" the
  range-finder fell through.
- Own-mech exclusion by minimum pick range (15) instead of the
  dcs_parent ancestry walk: sibling link edges made the FLOOR and SKY
  read as the player's subtree, so ground/sky aims never resolved.
  Measured: own barrels/flash live within ~8 of the eye; sky/ground
  are real pickable entities (sky.mod, afloor) -- firing into them
  fires and misses, per the operator's arcade experience.

EMU8000 core (emu8k.cpp): clamp the oscillator output after cubic
interpolation. Crest overshoot on UNFILTERED voices overflowed int32
in the volume multiply and wrapped the crest negative for 1-3 samples
-- the speech/effects crackle (recorded signature: +0.7 flipping to
-0.7 at crests, random phase vs the chunk grid). Filter paths already
clamped; the bypass path now matches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Cyd
2026-07-08 15:16:40 -05:00
co-authored by Claude Fable 5
parent d88d9212c0
commit e6ebef071d
2 changed files with 51 additions and 28 deletions
+11
View File
@@ -1796,6 +1796,17 @@ emu8k_update(emu8k_t *emu8k)
dat = EMU8K_READ_INTERP_CUBIC(emu8k, emu_voice->addr.int_address,
emu_voice->addr.fract_address);
#endif
/* Cubic interpolation overshoots past int16 on wave crests;
* UNFILTERED voices then overflow int32 in the volume
* multiply below (~39000 * 65535 > INT32_MAX) and the crest
* wraps NEGATIVE for 1-3 samples -- the audible "crackle"
* on loud speech/effects (recorded signature: +0.7 flipping
* to -0.7 exactly at crests). The filter paths clamp their
* output already; clamp the oscillator the same way. */
if (dat > 32767)
dat = 32767;
else if (dat < -32768)
dat = -32768;
/* Filter section */
if (emu_voice->filterq_idx || emu_voice->cvcf_curr_filt_ctoff != 0xFFFF) {
+40 -28
View File
@@ -808,6 +808,7 @@ static struct VScene {
unsigned geom_node; size_t geom_need, geom_stride; bool geom_active;
std::vector<float> geom_acc;
unsigned conn_node, conn_npolys, conn_loop; bool conn_active;
std::vector<int> conn_acc; /* whole-batch index staging */
/* action-26 texel upload assembly */
unsigned tex_node; size_t tex_need; bool tex_active;
std::vector<unsigned char> tex_acc;
@@ -1914,15 +1915,29 @@ static void scene_burst(const unsigned char *p, size_t n) {
return;
}
if (S.conn_active && action == 25) {
std::vector<std::vector<int> > &pl = S.polys[S.conn_node];
/* Accumulate the whole batch FIRST, then slice (mirrors the vertex
* path and Dave's proven parser). The old per-message slicing broke
* two ways on large geometries: a record straddling a burst boundary
* lost its tail AND misaligned every record after it, and a SECOND
* conn batch on the same geometry (floor = mixed tri+quad batches)
* terminated instantly against the total-poly count and dropped its
* indices -- the "holey ground" the reticle fell through. Also keep
* ALL loop indices (the old closing-dup drop halved every quad);
* genuinely closed loops just add a zero-area fan triangle. */
size_t nw = nb / 4;
for (size_t o = 0; o + S.conn_loop <= nw; o += S.conn_loop) {
std::vector<int> loop;
for (unsigned j = 0; j + 1 < S.conn_loop; j++) /* drop closing dup */
loop.push_back((int)rd_u32(d + (o + j) * 4));
pl.push_back(loop);
for (size_t o = 0; o < nw; o++)
S.conn_acc.push_back((int)rd_u32(d + o * 4));
if (S.conn_acc.size() >= (size_t)S.conn_npolys * S.conn_loop) {
std::vector<std::vector<int> > &pl = S.polys[S.conn_node];
for (size_t o = 0; o + S.conn_loop <= S.conn_acc.size();
o += S.conn_loop) {
pl.push_back(std::vector<int>(
S.conn_acc.begin() + o,
S.conn_acc.begin() + o + S.conn_loop));
}
S.conn_acc.clear();
S.conn_active = false;
}
if (pl.size() >= S.conn_npolys) S.conn_active = false;
return;
}
@@ -2134,7 +2149,13 @@ static void scene_burst(const unsigned char *p, size_t n) {
S.conn_loop = rd_u32(d + 8);
S.conn_active = (S.conn_npolys > 0 && S.conn_loop >= 2 &&
S.conn_loop <= 16);
S.polys[S.conn_node].clear();
S.conn_acc.clear();
/* do NOT clear S.polys here: geometries ship MULTIPLE conn
* batches (floor/walls = a triangle batch + a quad batch);
* clearing per header kept only the LAST batch. Fresh
* geometry arrives under fresh handles, so appending is
* safe; a re-flushed batch at worst duplicates triangles,
* which the raycast tolerates. */
}
break;
case 26: /* texel upload header [node][nbytes][w][h]... (4/8/16bpp) */
@@ -2278,24 +2299,15 @@ static bool raycast_pick(unsigned *inst_out, unsigned *dcs_out, unsigned *gg_out
S.inst_w3.find(inst);
if (wi != S.inst_w3.end() && wi->second != 2 && wi->second != 3)
continue;
/* Never pick the player's OWN articulation subtree (arms, torso,
* muzzle flash, searchlight): the ray grazes own geometry from
* the over-the-shoulder eye and a mid-volley pick of the muzzle
* flash retargets the salvo ("missiles hare off sometimes").
* Root DCS comes from the bridge's CAM line, 7th field. */
unsigned own_root = bridge_cam_root();
if (own_root) {
unsigned a = di->first;
bool own = false;
for (int hop = 0; hop < 64; hop++) {
if (a == own_root) { own = true; break; }
std::map<unsigned, unsigned>::const_iterator ppi =
S.dcs_parent.find(a);
if (ppi == S.dcs_parent.end() || ppi->second == a) break;
a = ppi->second;
}
if (own) continue;
}
/* Own-mech exclusion is by MINIMUM RANGE, not ancestry: walking
* S.dcs_parent (dcs_link edges) falsely flagged the FLOOR and
* SKY as the player's subtree (links also chain sibling scene
* DCSs in this dialect) -- ground/sky aims went no-hit and every
* such trigger boo-beeped. The player's arms and muzzle flash
* all live within ~12 of the eye; everything legitimately
* targetable is farther. Sky/ground are REAL pickable entities
* (sky.mod, afloor): firing into them fires -- and misses --
* exactly like the period pods. */
std::map<unsigned, unsigned>::const_iterator oi =
S.inst_object.find(inst);
if (oi == S.inst_object.end()) continue;
@@ -2339,7 +2351,7 @@ static bool raycast_pick(unsigned *inst_out, unsigned *dcs_out, unsigned *gg_out
m16_xform(world, &vv[o1], w1);
m16_xform(world, &vv[o2], w2);
float t = ray_tri(O, D, w0, w1, w2);
if (t > 0.0f && t < best_t) {
if (t > 15.0f && t < best_t) { /* min pick range */
best_t = t; best_inst = inst;
best_dcs = di->first; best_gg = gg; best_geo = geo;
}
@@ -2378,7 +2390,7 @@ static void scene_reset(void) {
S.dcs_parent.clear(); S.lights.clear();
S.view = VFrame();
S.geom_active = false; S.conn_active = false;
S.geom_acc.clear();
S.geom_acc.clear(); S.conn_acc.clear();
}
static void vpx_render_start(void) {