diff --git a/emulator/vpx-device/emu8k.cpp b/emulator/vpx-device/emu8k.cpp index d269982..4544b39 100644 --- a/emulator/vpx-device/emu8k.cpp +++ b/emulator/vpx-device/emu8k.cpp @@ -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) { diff --git a/emulator/vpx-device/vpxlog.cpp b/emulator/vpx-device/vpxlog.cpp index e361565..d062d41 100644 --- a/emulator/vpx-device/vpxlog.cpp +++ b/emulator/vpx-device/vpxlog.cpp @@ -808,6 +808,7 @@ static struct VScene { unsigned geom_node; size_t geom_need, geom_stride; bool geom_active; std::vector geom_acc; unsigned conn_node, conn_npolys, conn_loop; bool conn_active; + std::vector conn_acc; /* whole-batch index staging */ /* action-26 texel upload assembly */ unsigned tex_node; size_t tex_need; bool tex_active; std::vector 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 > &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 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 > &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( + 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::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::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) {