2232 Commits
Author SHA1 Message Date
libretroadmin 67db338ff4 draw: scaled 2D without a memset per pixel, a faster colour table
The software renderer's 2D drawing above 320 wide did a byte-sized
memset call for every source pixel, and scaled each source row up
again for every screen row it covers: 34000 calls a frame at
640x480, about 6% of the frame. Draw_PicScaled now scales a row
across once, with plain stores for the scales in use, and copies it
down; the transparent and character variants use the same stores,
and Draw_ConsoleBackground copies a row the background was scaled
up into from the one above.

The colored-lighting table (palmap2) is built by a search outward
from each colour's red in a palette sorted by red, seeded with the
neighbouring cell's colour, instead of against all 255 colours: the
same table, lowest index among equals, in under half the startup
time. Draw_BeginDisc no longer reads the disc pic before Draw_Init
has loaded it.

Every frame matches the previous build at 320x200, 640x480 and
1024x768, with colored lighting on and off, in the demos, menus,
console and view sizes; at 640x480 a frame takes about 6% fewer
instructions.
2026-10-12 23:33:17 +02:00
libretroadmin 29462b7079 sound: one pass less over the mix, and silence on the float output
The SFX mix is clamped to int16 full scale before the music is added;
with no music the transfer clamps to the same bounds itself (the
int16 blast saturates, the float blast now stops at 32767/32768 as
the clamp did), so that pass over the chunk is skipped. Output is bit
for bit the same, int16 and float.

When sound is not started, and on S_ClearBuffer, the float output
buffer is zeroed as well as the int16 one: with float output
negotiated, audio_step pushes that one, and would repeat the last
frame's samples.
2026-10-12 23:01:30 +02:00
libretroadmin cb2a244ef0 Vulkan: the SW renderer draws into staging, uploads ride the frame
The CPU no longer copies the frame, widens the Z buffer, re-copies
skins, or waits for an overlay upload:

- Each ring slot's staging is the SW renderer's frame buffer for that
  frame (vid.buffer, pointed at it in begin_frame), so the 8bpp frame
  reaches the GPU's copy with no host memcpy. Every pixel is drawn
  again each frame, so what the slot held two frames ago never shows.
  Staging prefers HOST_CACHED memory, since the renderer reads back
  what it draws.
- Z likewise: the renderer's 16-bit Z is drawn into the slot's Z
  staging (D_SetZBuffer re-points d_pzbuffer and its row table), and
  a small compute pass (zwiden.comp) widens it into the R32 vk_zbuffer
  on the GPU, in place of the host loop that read 2 and wrote 4 bytes
  a pixel (12 MiB a frame at 1080p) and the copy after it.
- Alias skins stay in a slot's skin pool across frames, as long as
  their cache block lives (dropped by the cache-invalidate hook), and
  the pool replaces the least recently drawn skin when full instead of
  failing; host_colormap stays too. A translated player colormap is
  still copied each frame it is drawn.
- Overlay pic uploads made during a frame are recorded into the slot's
  own upload command buffer and staging, and submitted ahead of the
  frame in its submit: no QueueSubmit and fence wait in the middle of
  Host_Frame, which also drained the frame in flight. The Setup menu's
  player preview is translated straight into staging, and only when
  the pic or the colours change, not every frame.

Output is unchanged: screenshots of the demos, menus, console and
view sizes match the previous build at 320x200 and 1024x768, with
compute rendering on and off, and the synchronization validation layer
reports nothing. On lavapipe, where the GPU is the CPU, the widen pass
costs about 1.5 ms a frame at 1024x768; on a GPU it is a fraction of
the host loop it replaces.
2026-10-12 23:01:26 +02:00
libretroadmin 8141003cdc Vulkan: frame fences, image hazards, sky and overlay descriptor sets
- A slot's frame fence is reset right before the submit that signals
  it, not when the slot comes round: a frame that does not submit
  (record failure, early return) leaves it signaled instead of making
  the next rotation wait on it forever. A failed submit signals it
  with an empty one.
- The barriers that discard the shared index, palette, Z and sky
  images, and the per-slot output image, wait for the stages an
  earlier frame still in flight reads them in (compute, the overlay's
  fragment shader, the frontend's transfer), instead of TOP_OF_PIPE,
  which ordered nothing: this frame's copies raced last frame's reads.
- The palette's transition is made visible to the overlay's fragment
  shader as well as the palette compute, which both sample it.
- A frame with sky spans but no particles, sprites, alias models or
  turb now runs the compute path: the spans were queued (and their SW
  draw skipped) but never dispatched.
- An overlay slot keeps its descriptor set across eviction and reuse.
  The pool cannot free sets one by one, and each re-upload allocated
  a new one: the main menu's animated cursor alone made 500 in a
  minute against a pool of 130. A slot whose upload fails part way is
  torn down instead of left holding an image nothing can evict.

The synchronization validation layer reported about 750 write-after-
read and 74000 read-after-write hazards in 400 frames of the first
demo; it reports none now.
2026-10-12 23:01:23 +02:00
libretroadmin 1913c4ec36 libretro: palette pass writes into the frontend's framebuffer
The software renderer keeps drawing into its own persistent 8bpp
vid.buffer; VID_Update's palette pass now asks the frontend for its
framebuffer (GET_CURRENT_SOFTWARE_FRAMEBUFFER, write-only) and
converts straight into it, so the frame reaches the video driver -
the threaded wrapper's frame memory, or the driver's upload buffer -
without another copy. Where none is lent it converts into finalimage
as before.

The pass converts the whole frame from vid.buffer, which holds every
region whatever the rect redrawn this frame, since a lent buffer
holds nothing of the frames before it, and reads rows at
vid.rowbytes.
2026-10-12 20:17:42 +02:00
libretroadmin e57bb11597 Update 2026-07-15 19:08:34 +02:00
libretroadmin 8f1e7dcdcd libretro: size the Quake hunk from frontend-reported free memory
The hunk was a fixed compile-time size (DEFAULT_MEMSIZE_MB, 8/24/32/64 MB
depending on platform and whether the Vulkan RHI is built). That is a
blunt instrument: too small and large mods hit Hunk_Alloc failures or
thrash Cache_FreeLow; too large and constrained targets over-commit.

Add an experimental environment callback, RETRO_ENVIRONMENT_GET_MEMORY_STATUS,
that reports the frontend's free and total physical memory (bytes), and use
it to size the hunk to the running machine. We take at most a quarter of the
reported free memory, cap it at 256 MB (ample for the biggest Quake mods,
and lazily committed so the reservation is cheap on virtual-memory
platforms), and only ever grow above the per-platform default - never below
it. The value is treated as an advisory snapshot, not a licence to allocate
everything, since "free" can include reclaimable cache.

Frontends that do not implement the query return false; in that case the
core keeps the exact defaults it uses today, so behaviour is unchanged
anywhere the callback is absent. The matching frontend side is a separate
RetroArch change wiring the call to frontend_driver_get_free_memory().
2026-07-01 06:44:59 +02:00
libretroadmin ff36964795 Makefile.common: use MSVC force-include syntax for the libvorbis rename
The float libvorbis TUs force-include fvorbis_rename.h to namespace their
symbols to fv_*.  The per-object CFLAGS used the GCC/clang "-include
<file>" form together with -Wno-maybe-uninitialized, both of which MSVC's
cl.exe rejects: -Wno-maybe-uninitialized trips command-line error D8021,
and -include is not understood (cl spells force-include "-FI<file>").

Select the force-include spelling by toolchain and drop the GCC-only
warning suppression on MSVC, where that diagnostic does not exist.  The
GCC/clang command line is byte-for-byte unchanged.
2026-06-30 03:17:03 +02:00
libretroadmin eac92862b5 jni: build float libvorbis as its own module; quiet Tremor warnings
ndk-build applies LOCAL_CFLAGS per module but ignores the per-file
target CFLAGS the standalone Makefile.common relies on, so the vendored
libvorbis sources were compiled without their include path or the fv_*
symbol-renaming -include header: bitrate.c could not find
vorbis/codec.h, and without the rename the float decoder would collide
with Tremor at link time.

Build the libvorbis sources as a dedicated static library (vorbis_fv)
that carries the rename header and include path in its own LOCAL_CFLAGS,
and link it into the main module, which now filters those sources out.
Also suppress the clang -Wduplicate-decl-specifier and -Wabsolute-value
warnings emitted by the vendored Tremor decoder.
2026-06-30 02:40:19 +02:00
libretroadmin b9bc99592a deps/libvorbis: align _book_unquantize signature with its declaration
The vendored float libvorbis (xiph/vorbis master) defines
_book_unquantize() with an ogg_int32_t sample-count parameter while
codebook.h declares it as int. On targets where ogg_int32_t and int
are the same underlying type this is silently accepted, but on
devkitARM (3DS), where ogg_int32_t resolves to a distinct type, the
declaration and definition conflict and the build fails.

Match the definition to the declaration (and to stable libvorbis
1.3.7, which uses int here). The count never exceeds int range, so
this is behaviour-preserving on every target.
2026-06-30 02:40:16 +02:00
libretroadmin 3f9112ab83 sound: correct the mixer clipping comments to match the code
The SFX clamp and music-add comments, inherited from the QuakeII/QuakeSpasm
lineage, described a scheme this mixer does not implement: "reduce by 6dB to
leave headroom", a smoothing lowpass that "will smooth out the clipping", and
"lower music by 6db".  None of that is applied here -- the paintbuffer is hard
-clamped to 0 dBFS, there is no lowpass, and music is summed at unity.

This is comment-only.  The behaviour is intentionally left as is: a hard clip
at 0 dBFS matches stock Quake's mix, the per-channel over-scale that used to
slam single sounds into the clamp is already fixed (leftvol capped at 255), and
the only clipping left is dense effects (and effects + music) summing past full
scale -- faithful behaviour, not a defect.  A soft limiter would colour valid
near-peak audio and split float from int16; a blanket -6 dB would make
everything permanently quieter.  So describe what the code does instead of
implying headroom that isn't there.
2026-06-30 02:05:01 +02:00
libretroadmin 271a2a745d sound: include vorbisfile.h by relative path in snd_vorbis_float.c
The ndk-build (Android.mk) does not honour the per-file target-specific CFLAGS
from Makefile.common -- it compiles every source with one uniform LOCAL_CFLAGS
-- so the -I$(LIBVORBIS_DIR)/include this TU relied on was absent there and
<vorbis/vorbisfile.h> was not found.

Include vorbisfile.h by a path relative to this source instead.  It is then
self-sufficient: vorbisfile.h includes "codec.h" relatively, and codec.h needs
only <ogg/ogg.h>, which is already on the include path wherever Tremor/libogg
build.  No -I of its own is required, so the per-file flag is dropped and the
header resolves under both the standalone Makefile and ndk-build.
2026-06-30 01:24:33 +02:00
libretroadmin 2b565437f9 menu: give Music Volume fine steps on all platforms
The in-game Music Volume slider stepped by 1.0 under _WIN32, so left/right only
toggled off/full, while every other platform (and the Sound Volume slider)
stepped by 0.1.  The coarse step dates to WinQuake driving the OS CD-audio
mixer, which was effectively on/off.  This core software-mixes music --
bgmvolume scales the streamed BGM in the paint path -- so there is no coarse
hardware limitation.  Drop the _WIN32 special case and step by 0.1 everywhere,
matching the sound slider; the slider already renders the continuous value.
2026-06-30 01:17:04 +02:00
libretroadmin a6dade8cc5 sound: linearly interpolate the SFX resampler (ResampleSfx)
ResampleSfx upsamples Quake's low-rate sound effects (most are 11025 Hz, some
22050) to the output rate with nearest-neighbour: it computed the fractional
source position in samplefrac but then truncated to srcsample >> 8 and dropped
the fraction, holding each source sample for several output samples.  That
zero-order hold is the main source of the effects' crunchy imaging.

Use the fraction that was already being computed: blend linearly between the
source sample and the next, in fixed point (samplefrac carries 8 fractional
bits, so the weight is 'samplefrac & 255').  s0 and s1 are in signed 16-bit
range so the blend stays in range, and (s1 - s0) * frac fits in int.  The upper
tap is clamped at the source end so the last fractional sample holds rather
than reading past the buffer.

This is all integer math, so the cached samples are bit-identical on every
platform/FPU and the SFX mix stays deterministic for runahead and netplay.  At
integer source positions (frac == 0 -- any down-sampling ratio, and the
stepscale == 1 fast path which is untouched) the output equals the old nearest
result, so only fractional/up-sampling ratios change.  Being a one-time
load-time resample, the hot mix loop and its cost are unaffected.
2026-06-30 01:10:27 +02:00
libretroadmin d7e1ca9aac libretro: correct the Sound Samplerate (Hint) description
The option text claimed higher rates lower latency and push aliasing above the
audible range; neither holds here.  Latency in the libretro path is buffered in
milliseconds, so a higher rate just stores more samples for the same time --
what actually helps is matching the device rate (Auto), which bypasses the
frontend resampler's filtering and group delay.  And Quake's sound effects are
low-rate sources upsampled with nearest-neighbour (ResampleSfx), so their
imaging sits at fixed low frequencies that a higher output rate does not move.
Reword to describe what higher rates do and do not do.
2026-06-30 01:10:22 +02:00
libretroadmin bcbb6bc045 libretro: add "Sound Samplerate (Hint)" option (backport from prboom)
Backport prboom's Sound Samplerate hint so the core can render at the host's
preferred rate instead of a framerate-derived one.  The option offers
auto/32/44/48/96 kHz (default auto); "auto" queries the frontend via
RETRO_ENVIRONMENT_GET_TARGET_SAMPLE_RATE and snaps to the nearest advertised
rate using the same thresholds as the prboom original (nearest_supported_rate).
Explicit values are taken verbatim.

tyrquake previously derived audio_samplerate solely from the framerate (certain
fps need a specific rate or the SFX resampler distorts).  The hint now owns the
rate; that framerate mapping is kept as framerate_to_samplerate() and reused as
the "auto" fallback for frontends that don't implement the target-rate call, so
those frontends see no behavioural change while ones that do now get their
target rate.  This is the one deviation from the prboom source, which falls back
to a bare 44100 -- here the framerate-appropriate rate is safer given tyrquake's
resampler sensitivity.

audio_samplerate widens from uint16_t to int (96000 doesn't fit in 16 bits).
Resolved at startup in update_variables, before SNDDMA_Init reads it; changing
the option takes effect on restart, consistent with the framerate option. The
guarded GET_TARGET_SAMPLE_RATE define matches the in-tree libretro.h value.
2026-06-30 00:56:59 +02:00
libretroadmin c8d904b6d0 sound: linearly interpolate the float music resampler
The streaming-music FIFO resamples codec output to the core rate by truncating
to the nearest source sample (src = i*scale), which zippers and repeats samples
whenever the rates differ -- e.g. 44.1 kHz music decoded into a 48 kHz core, or
the 22 kHz core option.  Now that the music lane is float, interpolate linearly
between adjacent source samples instead; float makes the interpolation exact and
music is not part of the deterministic SFX mix, so there is no determinism cost.

When the rates match (scale == 1.0) the fraction is 0 and every output equals
its source sample, so that common case is bit-identical to the old path -- the
change only affects actual rate conversion.  The per-format read is folded into
a small S_FetchFloat helper so the six nearest-neighbour branches collapse to a
single interpolating loop covering width 1/2/4 and mono/stereo.

This addresses fractional and up-sampling conversion (the 48 kHz case); integer
down-sampling (e.g. 44.1->22.05) still aliases and would need a decimating FIR,
which is intentionally out of scope.  The integer int16 output path is untouched
and stays nearest-neighbour and deterministic.
2026-06-30 00:41:51 +02:00
libretroadmin b462c20dbd sound: silence upstream libvorbis warnings without touching vendored source
Two -Wall warnings came from the vendored float libvorbis, not our code:
lpc.c's "*aut may be used uninitialized" (a known upstream false positive at
-O2) and the unused OV_CALLBACKS_* static structs that vorbisfile.h defines at
file scope and snd_vorbis_float.c doesn't use.

Keep the vendored tree pristine (so future libvorbis updates drop in cleanly)
and suppress narrowly instead: -Wno-maybe-uninitialized on the vendored objects
only, and a GCC/Clang diagnostic push/pop around just the vorbisfile.h include
in the wrapper.  snd_vorbis_float.c keeps full -Wall everywhere else; the pragma
is guarded on __GNUC__ so the MSVC C89 build is unaffected.
2026-06-30 00:41:46 +02:00
libretroadmin 0570d2b186 sound: decode FLAC straight to float on float output (recovers 24-bit)
On float output, decode FLAC to normalized [-1,1] float (width 4) instead of
casting every sample to int16.  FLAC delivers right-justified signed samples,
so scaling by 2^(bits-1) is exact for any depth.  This also lifts the 8/16-bit
restriction on the float path: 24- and 32-bit FLAC, previously rejected (and
silently truncated by the int16 cast in flac_write_func), now decode at full
precision through the S_RawSamples float lane.

The int16 output path is unchanged -- bit depth validation and the int8/int16
write callback are kept verbatim for it, gated on s_float_output.
2026-06-30 00:31:36 +02:00
libretroadmin f439db0d55 sound: decode Vorbis straight to float on float output via vendored libvorbis
When float audio output is negotiated, decode Ogg Vorbis with float libvorbis
(ov_read_float) instead of the fixed-point Tremor decoder, so music keeps the
full Vorbis precision rather than being truncated to int16.  Tremor stays the
default for the int16 output path: its integer IMDCT yields bit-identical PCM
on every platform/FPU, which the deterministic int16 lane depends on.

Both decoders are linked at once and selected per game at stream open from
s_float_output.  The catch is that Tremor and stock libvorbis export the same
public and internal symbol names (ov_*, vorbis_*, mdct_*, floor*_exportbundle,
res0_*, ...), so they cannot be linked together unrenamed.  The vendored
libvorbis is therefore symbol-namespaced to fv_* via fvorbis_rename.h (force-
included into its TUs), generated from the decode object set; the single
vendored libogg is shared by both and is deliberately left un-renamed.

Vendoring is decode-only.  The shared dsp clear/init and the mapping/registry
bundles reference the encoder (psy/envelope/smallft), but every such reference
is behind an encode-only guard (_vds_shared_init's if(encp), vorbis_dsp_clear's
if(b->ve)/if(b->psy), vorbis_info_clear's if(ci->psys), and mapping0_forward,
never dispatched when decoding).  Those symbols are satisfied by no-op stubs in
decode_stubs.c, keeping the encoder and its large masking/setup tables out of
the binary.  deps/libvorbis/lib is kept off the global -I path so its generic
header names don't shadow system headers (same hazard already noted for
deps/tremor); the vendored TUs get it via per-object CFLAGS.

snd_vorbis_float.c wraps the float decoder behind opaque void* handles so
snd_vorbis.c can dispatch open/read/rewind/close without including the
colliding libvorbis headers; it sets info.width=4, routing samples through the
S_RawSamples float lane added previously.  Net binary growth ~136 KiB.
2026-06-30 00:31:32 +02:00
libretroadmin f7b8a47689 sound: route streaming music through the float lane on float output
Add a float counterpart to the s_rawsamples music FIFO and select it by the
negotiated output format.  When float output is active S_RawSamples appends
into s_rawsamples_f (normalized [-1,1] * volume, via S_RawSamplesFloat, which
reads source width 4=float / 2=int16 / 1=uint8), and S_PaintChannels sums the
float music FIFO on the float side of the transfer: the clipped SFX paintbuffer
is transferred to the float output buffer, then the music is added on top and
re-clamped to [-1,1].

The SFX mix stays integer and deterministic; only the music lane goes float.
The int16 output path is unchanged -- the new lane is gated entirely behind
s_float_output, so with float output off S_RawSamples and S_PaintChannels run
the original integer code byte for byte.

This is the framework: every currently-built music codec (WAVE, FLAC, Vorbis
via Tremor) now plays through the float pipeline, with their int16 output
widened into the float FIFO.  Codecs that can decode straight to float at
higher-than-16-bit precision (24-bit FLAC, float libvorbis) are wired on top of
this in follow-up commits via the width-4 path.
2026-06-30 00:31:28 +02:00
libretroadmin 96fb8506c8 sound: optional float audio output via GET_AUDIO_SAMPLE_BATCH_FLOAT
Negotiate float audio output once at retro_load_game time through the
experimental RETRO_ENVIRONMENT_GET_AUDIO_SAMPLE_BATCH_FLOAT callback.
When the frontend answers true, the engine transfers the paintbuffer as
normalized float [-1,1] (Snd_WriteLinearBlastStereoFloat /
S_TransferStereoFloat) and audio_step pushes it through the float batch
callback; otherwise everything falls back to the existing int16 path
unchanged. Negotiation happens once per loaded game and the format is
never mixed; older frontends return false and keep the int16 path
transparently.

The paintbuffer accumulator carries 8 fractional bits, so normalizing by
256*32768 preserves sub-int16 precision the >>8 int16 blast would
otherwise discard. tyrquake's int16 blast hard-saturates rather than
soft-clipping, so the float blast hard-clamps to [-1,1] to keep the two
paths tonally identical -- verified float == int16/32768 to within one
LSB across a full accumulator sweep, with matching full-scale and
over-range clip behaviour.
2026-06-29 23:29:27 +02:00
libretroadmin 1def2c4e9a sound: fix Android NDK build (Tremor union magic, mixer shift UB)
Two issues surfaced by the Android (clang/bionic) build:

deps/tremor/misc.h defined `union magic` under two separate #if blocks,
`BYTE_ORDER==LITTLE_ENDIAN` and `BYTE_ORDER==BIG_ENDIAN`, with no #else.
Tremor only defines those BSD endian macros for _WIN32 and otherwise relies
on <sys/types.h> to pull them in; bionic's <sys/types.h> does not, so all
three expanded to 0, both 0==0 tests matched, and union magic was defined
twice -> "redefinition of 'union magic'".  Collapse the two blocks into a
single mutually-exclusive selection that picks the big-endian layout from a
reliable indicator (BSD BYTE_ORDER, the compiler __BYTE_ORDER__ builtin, or
common arch macros) and defaults to little-endian.  This builds on bionic
(little-endian) and keeps PPC64/BE correct via __BYTE_ORDER__ even when the
BSD macros are absent.

common/snd_mix.c clamped with `-32768 << 8`, i.e. a left shift of a negative
value, which is undefined behavior (-Wshift-negative-value).  Shift the
positive magnitude and negate: `-(32768 << 8)`; same -8388608 result, no UB.
2026-06-29 17:53:28 +02:00
libretroadmin b98afbe2e5 sound: don't put deps/tremor on the -I path (macOS <Block.h> clash)
Adding -I deps/tremor placed Tremor's generically named headers on the
angle-bracket include path.  On case-insensitive filesystems (macOS) a
system <Block.h> -- pulled in by CoreFoundation via CFBase.h -- then
resolves to deps/tremor/block.h instead of the Blocks runtime header, so
any TU that includes CoreFoundation (e.g. libretro-common/file/file_path.c)
fails with "unknown type name 'vorbis_block'".

The -I was never needed: the wrapper reaches Tremor through
<tremor/ivorbisfile.h> (-I deps) and Tremor's own sources self-include
their headers with quotes, which resolve relative to each .c.  Drop
-I$(TREMOR_DIR) so deps/tremor is no longer searched for angle includes.
2026-06-29 05:45:05 +02:00
libretroadmin 2aeb018560 sound: deterministic int16 Vorbis via vendored fixed-point Tremor
The vendored Vorbis decoder under deps/libvorbis was the float libvorbis.
ov_read decoded each frame to float (IMDCT/floor/residue are float by
design) and quantized to int16 on the way out, so the music output was a
wasted float->int16 round-trip and, worse, non-deterministic: the float
IMDCT yields different PCM across FPUs/platforms/compilers.  Editing
libvorbis cannot fix this -- Vorbis decode is float by design.

Replace it with the fixed-point Tremor (libvorbisidec) decoder, vendored
under deps/tremor.  Tremor decodes entirely in integer arithmetic and its
ov_read emits host-endian signed 16-bit PCM directly, so there is no float
pass and the int16 output is bit-identical on every platform.  The
snd_vorbis.c wrapper already had a VORBIS_USE_TREMOR path (ov_read without
the endian/width/signed args; ov_time_seek(0) is 0 either unit), so it is
used unchanged; only the build wiring and vendored sources change:

- deps/tremor: vendor the 13 Tremor decode TUs + headers (no _ARM_ASSEM_,
  full-accuracy integer math).  libogg stays (Tremor's ov_* API frames
  through it); the integer types come from <ogg/os_types.h>.
- Makefile.common: USE_CODEC_VORBIS now builds deps/tremor + libogg and
  adds the tremor/ + libogg includes instead of libvorbis.
- Makefile / jni/Android.mk: define VORBIS_USE_TREMOR; the main Makefile's
  VORBISLIB defaults to (and only accepts) tremor and links no system
  Vorbis library since the decoder is vendored.
- Delete deps/libvorbis: the float decoder is no longer used.

WAV (snd_wave.c, raw PCM + endian swap) and FLAC (snd_flac.c; libFLAC
hands int32 samples straight to an int16 copy) already decode as integers
with no float round-trip and are left as-is.

The core builds clean and links the Tremor decode with no system Vorbis
lib; a standalone harness decodes a 44.1 kHz stereo ogg through the
vendored Tremor + libogg to int16 bit-identically across runs (88200
frames == 2.0 s).
2026-06-29 03:53:12 +02:00
LibretroAdmin fabedcffc0 Fix MSVC C89 build: portable cache-line alignment macro
MSVC rejects GCC's __attribute__((aligned)) suffix syntax, producing
C2061/C2059/C2143 at the d_polyset_spans declaration in d_polyse.c and
cascading 'undeclared identifier' errors as the parser desyncs.

Add CACHE_ALIGN_DECL() in quakedef.h: __declspec(align()) prefix for
MSVC, __attribute__((aligned())) suffix elsewhere, wrapping the whole
declaration so each compiler gets the placement it expects. Both use
CACHE_SIZE so the alignment stays in sync with the pad-array math.
2026-06-14 15:35:32 +02:00
LibretroAdmin 5e10328faf r_aclip: batch clipped fan into a single D_PolysetDraw dispatch
R_AliasClipTriangle clips an alias triangle against the frustum
planes via Sutherland-Hodgman, producing a polygon of up to 7
vertices, then triangulates it as a fan around vertex 0.  The
old code emitted one D_PolysetDraw call per fan triangle, with
the source carrying a 'FIXME: do all at once as trifan?' note
above the loop.

All fan triangles already share the same pfinalverts buffer
(R_AliasClipTriangle's stack-local fv[pingpong]) and the same
facesfront flag (inherited from the input triangle), so batching
is mechanical: build a mtriangle_t array on the stack with
vertindex[0..2] = (0, t+1, t+2) for t in [0, k-3], set
numtriangles = k - 2, and dispatch once.

mtri_fan[5] covers the worst-case fan size: a 3-vertex triangle
clipped against 4 frustum planes (left, right, top, bottom) grows
to at most 3 + 4 = 7 vertices, yielding a 5-triangle fan.  Z-clip
is handled separately upstream and never inflates the polygon
past the per-plane growth of the XY clips.

Performance impact on the SW path is modest (per-call D_PolysetDraw
overhead is small after the static-spans change in the previous
commit), but the structural change matters for two reasons.  First,
the GPU compute path in D_PolysetDraw (Phase 5b-06: the
g_rhi->dispatch_3d_alias short-circuit) is dispatch-heavy -- one
GPU submit per fan triangle on a partially-clipped entity is
exactly what the unclipped batching in R_AliasPreparePoints
(commit message at r_alias.c:477) was written to avoid.  Bringing
the clipped path under the same batching treatment closes the
remaining per-triangle-dispatch hole.  Second, the loop is now
straight-line code with no global-state mutation inside the body
(no per-iter writes to r_affinetridesc.ptriangles or to a shared
mtri scratch), which simplifies reasoning about the rasterizer's
preconditions when reading this code.

Bit-identical output: the fan triangles are generated in the
same order ((0,1,2), (0,2,3), (0,3,4), ...) and consumed by
D_DrawNonSubdiv / D_DrawSubdiv in the same order (their inner
loops iterate 0..numtriangles-1 with the same per-tri math),
so the resulting pixel emissions are unchanged.

Builds clean on x86_64, aarch64, and armv7 NEON crosses.
2026-06-14 07:53:43 +02:00
LibretroAdmin 1dad65ff4d d_polyse: move D_PolysetDraw span scratch from stack to BSS
D_PolysetDraw used to declare its spanpackage_t spans[] working
buffer as a function-local CACHE_PAD_ARRAY of DPS_MAXSPANS + 1
entries.  At 64 B per spanpackage_t and DPS_MAXSPANS = MAXHEIGHT + 1
= 1201, that is 76928 B of stack frame -- enough to trip gcc's
stack-clash protection and force a probe loop in the prologue:

    leaq -73728(%rsp), %r11
.LPSRL0:
    subq $4096, %rsp
    orq $0, (%rsp)
    cmpq %r11, %rsp
    jne .LPSRL0
    subq $3352, %rsp

18 iterations of subq/orq/cmp/jne (one per 4 KB page) on every call.
This was the only function in d_polyse.c with a > 1 KB frame
(verified with -Wframe-larger-than=1024 -- all others are fine).

Move the scratch to a module-static BSS array, aligned to CACHE_SIZE
directly via __attribute__((aligned(CACHE_SIZE))) so the runtime
(uintptr_t)align trick is no longer needed.  D_PolysetDraw now
takes the array address straight from the static instead of
manufacturing it from %rsp + alignment math.

Prologue shrinks from 14 to 10 instructions before the a_spans
assignment; the entire 73728-byte probe loop is gone.  Bench of
an isolated function-call-with-large-frame harness shows ~0.18 ns
saved per invocation on Ryzen 9 9950X3D (1.82 -> 1.64 ns); modest
per-call but lossless and adds up across the 75-100 D_PolysetDraw
calls per typical frame.  Realer benefit is cache locality: the
same 76 KB physical buffer now stays in L1/L2 across consecutive
calls instead of being a fresh stack range each time.

Safe because D_PolysetDraw is rendering-thread-only (libretro keeps
audio/GS on separate threads that never touch the SW polyset path)
and does not recurse -- the rasterizers it dispatches to
(D_DrawSubdiv / D_DrawNonSubdiv -> D_PolysetRecursiveTriangle and
the per-row span emitters) do not call back into D_PolysetDraw.

Bit-identical output to the prior code -- pure storage relocation,
no math or control-flow change.  The buffer is fully overwritten
each call before it is read, so no stale-state issue between
invocations.  Builds clean on x86_64, aarch64, and armv7 NEON
crosses.
2026-06-14 07:53:39 +02:00
LibretroAdmin f660c84598 r_edge: replace R_BeginEdgeFrame per-row clear with memset
Addresses an in-source FIXME ("FIXME: set with memset") on the
per-frame initialiser for the newedges / removeedges scanline-edge
list arrays.  The old loop did two sequential pointer writes per
scanline:

  for (v = r_refdef.vrect.y; v < r_refdef.vrectbottom; v++)
      newedges[v] = removeedges[v] = NULL;

For an N-row viewport this is 2N stores under -O2 (or 4-wide
vectorised stores under -O3), where N runs ~240-1200 depending on
the user's resolution.  glibc memset uses rep stos / AVX2 stores
and consistently outperforms the compiler's loop expansion by
~2-4x for clears of this size.

Add defensive bounds-clamp on y_lo / y_hi (the arrays are sized
MAXHEIGHT, so a vrect that somehow crossed that bound would be a
serious bug elsewhere; the clamp turns it from heap corruption
into a no-op clear).  No semantic change otherwise -- NULL is
all-zero-bits on every platform tyrquake supports.

Wins are small in absolute cycles (~500 cycles/frame at typical
viewport sizes), but the FIXME predates the modern compiler era
and was always going to be a one-line cleanup.  Builds clean on
x86_64 native and aarch64 cross.
2026-06-14 07:41:25 +02:00
LibretroAdmin c48c7f01c7 model.h: reorder msurface_t fields by per-frame access pattern
128-byte struct (two 64-byte cache lines on x86_64 / aarch64) was
partitioned with hot and cold fields mixed across both lines: the
old layout put firstedge/numedges/cachespots[0] in line 0 (cold,
only touched at render time) while dlightframe/dlightbits/styles
sat in line 1 (hot, touched per-frame during R_MarkLights for
every surface in the camera PVS).  This forced every visibility
pass to pull line 1 for surfaces it would then frustum-cull and
never render.

Reorder so that the per-frame BSP-traversal + culling + dlight
phase only ever needs to read cache line 0, with line 1 reserved
for the per-render-only fields (edge walk, texture vecs, lightmap
data, surface cache).  Sizes unchanged at 128 B on 64-bit and
96 B on 32-bit; field offsets verified to match the intended
partitioning on both x86_64 and aarch64.

  Line 0 (offsets 0..63):  visframe(0), clipflags(4), flags(8),
    dlightframe(12), mins(16..27), styles(28..31), maxs(32..43),
    plane(48), dlightbits(56..63)
  Line 1 (offsets 64..127): firstedge(64), numedges(68),
    texturemins(72), extents(76), texinfo(80), samples(88),
    cachespots(96..127)

No code change anywhere else: every consumer accesses these by
name (out->firstedge etc., per Mod_LoadFaces_BSP29 and the entire
renderer), so the reorder is mechanically transparent.  Confirmed
by grep audit: no sizeof(msurface_t) / memcpy / memset / offsetof
sites in common/.  All TUs compile clean on x86_64 native,
aarch64 cross, and armv7 NEON / no-NEON cross.

Magnitude: the win lands on PVS-visible-but-frustum-culled
surfaces, which never touch line 1 in the new layout.  Cold-cache
case (long traversal of large outdoor scene where the surface
array overflows L2): ~1us/frame fewer line-1 misses for the
~50% of PVS surfaces typically rejected at the frustum step.
Warm-cache case (interior scene fitting in L1): essentially
neutral.  Always-on across both rasterizer and Vulkan paths.

The field-cluster comment in the struct documents the access
phase each field belongs to, so future additions land in the
right line.
2026-06-14 07:31:54 +02:00
LibretroAdmin 68be6df709 r_alias: SIMD R_AliasDrawShadow per-vertex projection (SSE2 + AArch64 NEON)
gcc -O3 rejects the per-vertex projection loop in R_AliasDrawShadow
with 'control flow in loop' (depth >= 4 near-plane clip + the
conditional stores it gates).  At the project default -O2 gcc
emits straight scalar fp.  Each iteration is heavy: 3 byte-decompress
mul-adds, 4 yaw-rotation muls + 2 adds, two divisions
(t = (fz - w[2]) / Lz and 1/depth), 9 mul-add dot-product terms,
and 4 conditional output stores.  ~14 ns/vert scalar; with
~10 visible alias entities x ~400 verts each per frame this is
~55 us/frame when r_shadows is enabled.

Manual SIMD turns the near-plane clip into a mask and processes
4 verts in parallel.  Two micro-optimisations apply to both the
SIMD and the scalar tail:

  - inv_light_z = 1.0f / R_SHADOW_LIGHT_Z hoisted out of the loop;
    the per-vert / becomes *.
  - dz_const = floor_z - r_origin[2] is constant across the loop
    (the third coordinate of every shadow vertex is floor_z, so
    delta[2] is fixed), and the three products dz_dot_{r,u,p}
    against vright[2]/vup[2]/vpn[2] are likewise constant.  The
    per-vert DotProducts reduce to two mul-adds plus the constant.

Bench at -O2 (tyrquake default), x86_64, n=400:

    scalar:        13.81 ns/vert  (5523 ns/call)
    SSE2 (tight):   4.87 ns/vert  (1947 ns/call)   2.84x

  Correctness: 100/100 random-input trials (yaw, floor_z, byte
  vertex data, n in [5,505]) match scalar within eps=1e-3 in
  screen-pixel space -- bit-identical after the int casts that
  D_DrawShadowTriangle does on these floats.

The SIMD path uses a 16-byte raw load of pverts[i..i+3] (trivertx_t
is byte-packed 4-bytes-per-vert so 4 verts = one __m128i load) +
a fixed unpack pipeline to deinterleave the four x/y/z components
into separate float lanes.  No scalar gather setup.

AArch64 NEON port uses vld4_u8 which decodes 16 bytes as four
4-byte component vectors in a single instruction (where SSE2 needs
four unpack steps), then vdivq_f32 for the inverse depth (aarch64
has full hardware divide; ARMv7 doesn't and would need vrecpeq +
Newton-Raphson refinement).  Per the existing alias-file
convention from ddad8ca, ARMv7 NEON falls back to the scalar tail
rather than carrying a third precision-managed code path.

Per-frame impact when r_shadows is enabled and the user has
~10 visible alias entities x 400 verts each: ~36 us/frame saved.
Heavy scenes (30 monsters in view): up to ~134 us/frame.
Zero impact when r_shadows is off (the surrounding function never
runs).

The per-pixel rasterizer D_DrawShadowTriangle is NOT SIMD'd in
this commit.  Its inner loop does

    if (z_int >= zrow[x]) prow[x] = cmap[prow[x]];

which is a conditional gather (cmap indexed by current pixel
value) and conditional byte store -- the same fundamental obstacle
as Mip0Stuff(MIPRGB) and the gather-bound Turbulent8 fast path.
SSE2 has no efficient gather for this access pattern; manual SIMD
on the math part costs more than it saves (verified earlier in
this audit cycle with the kernel-dithering and liquid-blend bench
which showed the same shape).

Builds clean: x86_64 native, aarch64 cross, armv7 NEON cross
(picks scalar tail), armv7 no-NEON cross (full scalar).
2026-06-14 07:06:10 +02:00
LibretroAdmin 9ec95495a4 Makefile: -O3 override for snd_mix.c and r_alias.c hot inner loops
Audit of -O2 vs -O3 -fopt-info-vec-optimized across all 22 hot
rendering/audio TUs surfaced exactly two files where gcc finds
real auto-vectorizable loops at -O3 that it leaves scalar at the
project default -O2.  Cheaper to per-file flag-bump these than
to write SSE2+NEON+scalar by hand for the small absolute win.

snd_mix.c
  - common/snd_mix.c:261 paintbuffer saturate-clamp loop:

        for (i = 0; i < chunk; i++) {
            paintbuffer[i].left  = CLAMP(-32768<<8, paintbuffer[i].left,  32767<<8);
            paintbuffer[i].right = CLAMP(-32768<<8, paintbuffer[i].right, 32767<<8);
        }

    gcc -O3 emits 16-byte SSE2 / NEON max+min on the two i32 lanes
    per stereo sample.  -O2 leaves it scalar.

  - common/snd_mix.c:273 BGM stream mix-in loop:

        for (i = 0; i < bgm_count; i++) {
            int s = (s_rawhead + i) & (MAX_RAW_SAMPLES - 1);
            paintbuffer[i].left  += s_rawsamples[s].left;
            paintbuffer[i].right += s_rawsamples[s].right;
        }

    Also 16-byte vec at -O3; runs only while music is actually
    playing through the bgmusic codec layer.

  - The pair-of-scalar-tail loops at lines 110 and 401 are the
    residual handlers for the SSE2/NEON SIMD added in 002deb9
    (snd_mix SIMD).  Those run for <4 samples on each call and
    autovec-status doesn't matter; included in this bump for
    free because they're in the same TU.

r_alias.c
  - common/r_alias.c:1113 R_AliasBlendPoseVerts pose-interp lerp:

        for (i = 0; i < hdr->numverts; i++, poseverts++, pv1++, pv2++, light++) {
            poseverts->v[0] = (pv1->v[0] * blend0 + pv2->v[0] * blend1) >> 22;
            poseverts->v[1] = (pv1->v[1] * blend0 + pv2->v[1] * blend1) >> 22;
            poseverts->v[2] = (pv1->v[2] * blend0 + pv2->v[2] * blend1) >> 22;
            poseverts->lightnormalindex = light->lightnormalindex;
        }

    Standalone bench at typical Quake numverts=400 (release flags,
    Ryzen 9 9950X3D):

        scalar -O2:  1260.5 ns/call  (3.15 ns/vert)
        scalar -O3:   886.9 ns/call  (2.22 ns/vert)   1.42x

    Per frame at 10 visible alias entities x 400 verts each:
    ~4us at 60Hz.

  - Two other O3-only sites in r_alias.c are cold: the stverts
    store loop at line 213 is one-shot at alias-model load time,
    and the t2matrix 3-iter SLP loop at line 587 trivial.  Bumped
    along with the rest of the TU for free.

Combined release-build win: ~5us/frame on typical scenes.

Gated on DEBUG != 1.  In debug builds CFLAGS has -O0 set, and
unconditionally appending -O3 would override it and defeat -g
optimization-off semantics.  No effect on platforms whose CFLAGS
already pin -O3 (e.g. libnx) -- last -O wins on the gcc command
line, and -O3 -O3 == -O3.

Verified: snd_mix.o and r_alias.o pick up -O3, no other TU does,
DEBUG=1 produces -O0 with no -O3 leak.  Full common/*.o build
(87 TUs) compiles clean at default platform=linux.

Rest of the autovec-study audit reached saturation -- no further
manual-SIMD candidates remained that would save >10us/frame after
the R_AddDynamicLights SIMD landed in 0cc23c7.  This is the only
remaining flag-bump-level win.
2026-06-14 06:55:54 +02:00
LibretroAdmin 0cc23c798b r_surf: SIMD R_AddDynamicLights inner loop (SSE2 + NEON)
gcc -O3 reports 'control flow in loop' / 'vectorization is not
profitable' on the inner s-loop of R_AddDynamicLights -- the two
nested branches (abs(sd), sd>td vs sd<=td) plus the conditional
store-and-add defeat autovec.  Manual SIMD turns each branch into
a mask and the conditional store into a masked add:

  for (s = 0; s < smax; s++) {
      sd = local[0] - s*16;  if (sd < 0) sd = -sd;
      if (sd > td) dist = sd + (td >> 1);
      else         dist = td + (sd >> 1);
      if (dist < minlight)
          blocklights[t*smax+s] += (rad - dist) * 256;
  }

becomes (SSE2):

  s_v       := {s+0, s+1, s+2, s+3};
  sd_v      := abs(local0 - s_v*16);            /* arith-shift trick */
  gt        := cmpgt(sd_v, td_v);
  dist_v    := (gt & (sd+td/2)) | (~gt & (td+sd/2));
  dist256   := dist_v << 8;
  mask      := cmpgt(minl*256, dist256);
  delta     := mask & (rad*256 - dist256);
  bl[s..s+3] += delta;

Two pre-passes outside the dlight loop keep the inner work all-int:
rad_i = (int)(rad*256), minlight_i = (int)(minlight*256), local0 =
(int)local[0].  The scalar form already cast '(rad-dist)*256' to
int inside the conditional store and used integer texturemins
subtractions on local[0]/local[1], so the cast is just hoisted.

NEON path uses the algebraic identity
  max(sd,td) + min(sd,td)/2  ==  if (sd>td) sd+td/2 else td+sd/2
(sd and td are both non-negative after the abs steps; the two
branches agree at sd==td), which saves one compare + the and/andnot
blend that SSE2 needs.

Correctness:
  - Bench harness (1000 random-input trials, smax in [1,30],
    local0/td/rad/minlight randomized) matches scalar bit-exactly.
  - Re-verified in-tree (5000 trials with the patched form
    transcribed from r_surf.c): 5000/5000.

Perf at -O2 (tyrquake default), x86_64:
  smax=16:  scalar 26.3 ns/call (1.64 ns/iter)  ->  SIMD 11.5 ns (0.72 ns/iter)  2.3x
  smax=64:  scalar 76.9 ns/call (1.20 ns/iter)  ->  SIMD 39.5 ns (0.62 ns/iter)  1.95x

Typical scene: ~100 dlit-surfaces x ~2 dlights x ~50 inner iters
= ~10k iter/frame; estimated frame savings ~25us at SSE2 speeds.
ARMv7 NEON / aarch64 expected similar shape; NEON correctness path
not separately benched per the established convention (mechanical
translation of the same algebra, NEON kernels validated by
aarch64-gcc compile-check only).

R_AddDynamicLightsRGB has the same outer shape and would benefit
from the same SIMD on the dist-compute (its store body adds 3
channel multiplies per pixel which complicates the SIMD store, and
it's only reached when r_coloredlight is enabled which is off by
default).  Leaving RGB on the scalar path for now.

Builds clean: x86_64, aarch64, armv7 NEON, armv7 no-NEON
(scalar-tail fallback).  No new files; LMAP_SSE2 / LMAP_NEON
macros from the earlier R_BuildLightMap[RGB] SIMD work
(ddad8ca / 19f0469) reused as-is.
2026-06-14 06:10:41 +02:00
LibretroAdmin 8881ef9a00 heap audit round 8: fix CL_KeepaliveMessage stack overflow
Stack buffer overflow in CL_KeepaliveMessage's net_message backup,
reachable from a network source.

    static void
    CL_KeepaliveMessage(void)
    {
        ...
        byte olddata[8192];           // <- under-sized
        ...
        old = net_message;
        memcpy(olddata, net_message.data, net_message.cursize);  // overflow

net_message is SZ_Alloc'd in NET_Init to NET_MAXMESSAGE
(= MAX_MSGLEN = 32768).  net_message.cursize can take any value in
[0, NET_MAXMESSAGE].  When cursize > 8192 the memcpy writes past
olddata into the saved frame pointer / return address on the stack.

CL_KeepaliveMessage is called from CL_ParseServerInfo's model and
sound precache loops (and only those loops).  The net_message it
backs up is the serverinfo blob itself, which routinely approaches
the 32 KiB capacity: MAX_MODELS (256) and MAX_SOUNDS (256)
precache entries at up to MAX_QPATH (64) bytes each adds 32 KiB of
strings before any of serverinfo's other fields are counted.

Attacker-controlled.  A malformed-but-protocol-valid serverinfo
whose cursize falls in the (8192, NET_MAXMESSAGE] range turns the
keepalive backup into a remote stack overflow during the very next
keepalive tick (i.e. during the loading screen, before the user
sees any game frames).  The corresponding restore-side memcpy at

    net_message = old;
    memcpy(net_message.data, olddata, net_message.cursize);

reads from the same under-sized buffer, giving an additional
stack-info-leak primitive once the overflow path is conditioned
on something other than the read return address.

Fix: size olddata to NET_MAXMESSAGE to match the source buffer,
plus a defensive Host_Error in front of the memcpy in case some
future change to the SZ_GetSpace invariants (or a stomp on
net_message by an unrelated bug) lets cursize fall outside
[0, sizeof(olddata)].  The +24 KiB stack frame is fine in this
call site -- CL_KeepaliveMessage is leaf-adjacent in the host
loop, not nested.

Builds clean on x86_64, aarch64, armv7.
2026-06-14 05:25:00 +02:00
LibretroAdmin 237abee785 heap audit round 7: bound BSP loader count*sizeof multiplications
A class of bug adjacent to d0f7e07 (Hunk_Alloc INT_MAX bound) but
upstream of it.  The brush-model loaders in Mod_LoadBrushModel
derive

    count = l->filelen / sizeof(*in)
    out = Hunk_Alloc(count * sizeof(*out))

When sizeof(*out) > sizeof(*in) the int multiplication can wrap in
32-bit signed arithmetic before reaching Hunk_Alloc.  d0f7e07
catches the negative-wrap case (size > INT_MAX - sizeof(hunk_t) -
15), but a count chosen so that count * sizeof(*out) wraps to a
small positive int -- or to exactly zero -- passes the allocator
check.  The subsequent for (i = 0; i < count; i++) ... walk then
writes count * sizeof(*out) bytes past the under-allocation.

Worked example, Mod_LoadFaces_BSP29 (sizeof bsp29_dface_t = 20,
sizeof msurface_t = 128, 6.4x growth):

    file_size               = 670 MiB  (well under INT_MAX)
    faces lump filelen      = 670 MiB
    count = filelen / 20    = 33,554,432  (= 2^25)
    count * 128             = 2^32       (wraps to 0 in int32)
    Hunk_Alloc(0)           = small/empty pointer
    walk loop writes        = 33M * 128 B = 4 GiB

Eleven loaders affected; growth ratios in descending order:

    Faces_BSP29           20 ->128   6.4x
    Faces_BSP2            28 ->128   4.6x
    Marksurfaces_BSP29     2 ->  8   4.0x
    Nodes_BSP29           24 -> 80   3.3x
    Edges_BSP29            4 -> 12   3.0x  (also (count+1) alloc)
    Nodes_BSP2            32 -> 80   2.5x
    Marksurfaces_BSP2      4 ->  8   2.0x
    Planes                20 -> 40   2.0x  (count * 2 * sizeof)
    Clipnodes_BSP29        8 -> 12   1.5x
    Edges_BSP2             8 -> 12   1.5x  (also (count+1) alloc)
    Texinfo               40 -> 56   1.4x

Leaves were already bounded against MAX_MAP_LEAFS in an earlier
round.  Vertexes / Submodels / Clipnodes_BSP2 / Surfedges have
sizeof(*out) == sizeof(*in) and so cannot wrap (count *
sizeof(*out) <= filelen <= INT_MAX); skipped to keep the patch
focused on the genuinely-vulnerable sites.

Adds a Mod_BoundLoaderCount helper near the top of model.c:

    Mod_BoundLoaderCount(int count, int reserve_slots,
                         size_t record_size,
                         const char *fnname, const char *modname)

verifying count >= 0, reserve_slots >= 0, count + reserve_slots
doesn't overflow int, and (count + reserve_slots) * record_size
<= INT_MAX.  SV_Errors on any violation naming the offending
lump.  reserve_slots handles the (count + 1) edge allocations;
record_size accepts a compile-time multiplier (planes pass
2 * sizeof(*out)).

No behaviour change on well-formed BSPs.  Bound math unit-tested
across 15 boundary, attack, and degenerate inputs.

Builds clean on x86_64, aarch64, armv7.
2026-06-14 05:15:58 +02:00
LibretroAdmin 814cb32b1f r_surf: scrub dead R_BuildLightMapRGB bound/invert/shift block
The function carried a multi-line block-comment containing an
abandoned 'bound, invert, and shift' formula that was never enabled
(commented out long ago).  Removing it now that the live clamp path
is one R_LightMapClampRGB call -- git history preserves the dead code
if anyone needs to revive that formula.

Also dropped 'int shifted;' which was assigned 0 once and read once
in the clamp's '>> shifted' fold (already folded out in the previous
commit's helper call).  Inlined 'sample' initialization into its
declaration.  Tightened the trailing comment and indentation around
the dynamic-light call to match the rest of the file.

No behavior change.  Library size unchanged.
2026-06-14 04:06:53 +02:00
LibretroAdmin 19f04692e4 r_surf: SIMD R_BuildLightMapRGB (reuse existing helpers + new clamp)
R_BuildLightMapRGB has the same three-loop structure as R_BuildLightMap
(fill ambient, accumulate lightmap*scale, bound) but with three changes:
 - size is smax*tmax*3 instead of smax*tmax (RGB triple)
 - the accum loop is hand-unrolled with i+=3 doing 3 sequential updates
 - the bound loop is a plain int clamp(x, 256, 65536), not the
   max((255*256 - x) >> 2, 64) formula used by the non-RGB path

The unrolled accum loop is net-identical to bl[i] += lightmap[i] * scale
for all i in [0, size) -- it's just 3x manual unroll of stride-1
updates.  So R_LightMapAccum can be called directly with size=smax*tmax*3
and produces the same result with the SIMD it already has.

R_LightMapFill is reusable as-is for the ambient fill.

For the clamp bound, added a new R_LightMapClampRGB helper:
 SSE2: cmplt + and/andnot blend for max(x, lo); cmpgt + blend for
       min(x, hi).  No PMINSD/PMAXSD in SSE2.
 NEON: native vmaxq_s32 + vminq_s32 in one chain.

In the caller, 'shifted' is unconditionally 0 (the local is assigned
once and never reassigned), so 'bl[i] >> shifted' folds to 'bl[i]' --
the helper takes just (lo, hi) without a shift parameter.  Cast-to-void
preserved on shifted for readability in case future changes restore
non-zero shift behavior.

Bench at -O2 on the new clamp loop (NSZ=972, 50000 reps):
  x86_64 scalar  : 36.89 ms  (1317 Mints/s)
  x86_64 SSE2    : 18.62 ms  (2610 Mints/s) -- 1.98x
  aarch64 scalar : 384.6 ms qemu (NEON: 131.9 ms qemu, 2.92x qemu-noise)
  armv7 scalar   : 319.8 ms qemu (NEON: 127.8 ms qemu, 2.50x qemu-noise)
At -O3 gcc auto-vectorizes the clamp to similar code; manual ties autovec
(no regression).

Cleanup: removed the now-unused 'int r' local, replaced the three open-
coded loops with helper calls.  Preserved the surrounding commented-out
'bound, invert, and shift' block and the leilei artifact-fix note in
spirit.  Function body shrinks from 89 lines to 49.
2026-06-14 03:57:52 +02:00
LibretroAdmin ddad8ca226 r_alias: SIMD R_AliasTransformAndProjectFinalVerts (SSE2, AArch64 NEON)
Manual 4-wide SoA-across-vertices kernel for the alias transform/project
loop.  Beats gcc -O3 autovec by 1.45-1.50x on x86_64 native.

Approach: load 16 bytes (4 trivertx_t) per batch, transpose byte fields
into per-component vectors (vx[0..3], vy[0..3], vz[0..3], lni[0..3]),
run transform + 1/z + projection + lighting in parallel as SoA, scatter
results into AoS finalvert_t.  SSE2 uses _mm_unpacklo/hi_epi8/16 chains
+ shuffle-based 4x4 transpose; AArch64 NEON uses a single vqtbl1q_u8
per component.

Why beat autovec: gcc -O3 autovectorizes this loop to 16 vertices/batch
on both ISAs (132 NEON / 431 packed SSE2 insns), but the wide shape
exceeds register count and spills heavily to stack.  A narrower 4-wide
kernel fits cleanly (308 NEON / 629 SSE2 insns, no spills) and wins
even against -O3 autovec.  Tyrquake builds at -O2 default where gcc
emits NO SIMD for this function, so the win is real for production.

Bench on x86_64 native (Ryzen-class, NVERT=200, NMODELS=1000, 200 reps):
  -O2 scalar:           1.92 ms/iter (autovec OFF at -O2)
  -O3 scalar (autovec): 1.88 ms/iter
  Manual SSE2:          1.29 ms/iter -- 1.49x vs -O2, 1.46x vs -O3
Fuzz-verified byte-exact against scalar across all sizes 1..200.

AArch64 NEON: codegen is 308 SIMD insns (vs gcc autovec's 525), shape
matches SSE2 win.  QEMU benchmarks are unreliable for NEON throughput
(every NEON op costs multiple host insns) so no qemu numbers; expect
similar real-hardware win as x86_64.  Recommend bench on real AArch64
HW before judging.

ARMv7 NEON gated off: 16 q-regs vs AArch64's 32 means the 4-wide shape
would spill, and no real-HW bench available.  ARMv7 falls back to
scalar (same as before this patch).

Internals:
- Static helpers R_AliasTransformAndProject4_{SSE2,NEON} process 4
  vertices each; gcc inlines them at -O2 into the call site.
- C89 form: all decls (with initializers) at top of block, then the
  AoS scatter stores as statements.  _MM_TRANSPOSE4_PS expanded inline
  as 8 shuffle initializers so the cast+convert stays declaration-form.
- Scalar tail unchanged from original: handles numverts % 4 in [0..3].
- finalvert_t.reserved is not touched (matches scalar -- rhi.h notes
  the field is ignored).

The autovec scalar at -O3 is actually 2% SLOWER than scalar at -O2
+-fno-tree-vectorize on this function -- gcc's wide autovec attempt
introduces overhead it cannot pay back.  This is part of why the
manual narrow kernel wins: it captures the SIMD opportunity that
autovec sees but executes with overhead autovec adds.
2026-06-14 03:44:23 +02:00
LibretroAdmin 7f955f37c4 r_surf: SIMD R_BuildLightMap inner loops (SSE2, NEON)
R_BuildLightMap composes the per-surface lit cacheblock that
D_DrawSpans16 then samples.  It runs once per drawn surface from
R_DrawSurface (hundreds of times per frame; with dynamic lights, on
every dlit surface every frame).  Three inner loops over
blocklights[smax*tmax] (typical size 324 ints):

  (1) clear to ambient int constant
  (2) blocklights[i] += lightmap[i] * scale   (byte * int + accum)
  (3) t = (255*256 - blocklights[i]) >> 2; if (t < 64) t = 64

Add LMAP_SSE2/LMAP_NEON detection and three static helpers, each
SIMD-gated with scalar fallback.  The accumulate kernel uses
PMADDWD on SSE2 (after widening bytes to int32 stride so adjacent
int16 pairs PMADD as m_i*scale + 0*0) and vmlaq_s32 on NEON.  Both
process 16 bytes per outer iter -- matching the shape gcc -O3
autovec produces for the scalar loop, so the SIMD path ties autovec
at -O3 and beats scalar at -O2.

Bench at -O2 (tyrquake's default), 324 ints, gcc 13 / qemu-user for
ARM:

                 SSE2     AArch64    ARMv7
  loop 1 (fill)  4.01x     3.50x     3.06x
  loop 2 (accum) 4.30x     2.05x     1.65x
  loop 3 (bound) 2.56x     2.14x     1.71x

Universal win.  At -O3 gcc auto-vectorizes the scalar to equivalent
code (ties on every ISA, no regressions).  ARMv7-without-NEON
(-mfpu=vfpv3) falls through to scalar with zero NEON q-regs emitted,
verified via cross-objdump.

PMADDWD soundness note: SSE2 path treats scale as a 16-bit lane,
which is safe because scale's source -- d_lightstylevalue[] -- maxes
at (25 letters * 22) = 550 for animated styles (256 unanimated).
The comment in R_LightMapAccum flags this for the case where the
engine ever grows brighter lightstyles.

R_BuildLightMapRGB has the same fill/accum shape and could reuse the
helpers, but its bound loop is structurally different (uses sample/
shifted clamp instead of the 64..65280 invert).  Left for follow-up.

Correctness fuzz: sizes {0..8, 12, 17, 100, 256, 324} pass byte-exact
against scalar across SSE2 / AArch64 NEON / ARMv7 NEON kernels.
2026-06-14 03:10:22 +02:00
LibretroAdmin 002deb9bc0 snd_mix: SIMD audio mixer + linear-blast inner loops
Two SIMD wins in the audio mixer using the codegen-comparison
methodology from d_scan's D_DrawZSpans pass.  Both inner
loops are gather-free, so the only thing standing between
them and SIMD was finding the right intrinsic shape and
verifying it actually beats gcc's -O3 auto-vectorizer on
each target ISA.

(1) Snd_WriteLinearBlastStereo16 -- universal win

The loop is "snd_out[i] = clamp(snd_p[i] >> 8, INT16_MIN,
INT16_MAX)".  Shift-then-saturating-narrow has a dedicated
single-instruction primitive on every modern SIMD ISA:

  SSE2  : PACKSSDW  (two int32x4 -> one int16x8, saturate)
  NEON  : VQSHRN.s32 (int32x4 -> int16x4, shift+saturate
                      in one insn) + VCOMBINE

gcc -O3's auto-vectorizer DOES vectorize the scalar loop on
all three target ISAs, but it does NOT recognize the
saturating-narrow primitive.  It emits the long-hand:
"shift, compare-greater-than against each clamp boundary,
select".  That's a 4-5 insn dependency chain per lane vs a
1 insn primitive.

Bench (gcc 13 -O3, 1 Msample input, 200 iters):

  x86_64 SSE2  : scalar 0.638 ms  sse2 0.250 ms  2.55x
  AArch64 NEON : scalar 3.412 ms  neon 2.908 ms  1.17x
  ARMv7   NEON : scalar 4.354 ms  neon 2.556 ms  1.70x

The AArch64 ratio is the smallest because its auto-vec
already does saturate-via-cmgt+select fairly tightly.
ARMv7 wins biggest because its auto-vec is the loosest of
the three.

Detection: SMIX_NEON covers both AArch64 and ARMv7 NEON
(broad gate -- the saturating-narrow primitive is in the
ARMv7 NEON ISA, no AArch64-only intrinsics used).

(2) SND_PaintChannelFrom16 -- SSE2 + AArch64 only

The loop is the classic stereo MAC: per int16 sample, add
sample*leftvol and sample*rightvol into the paintbuffer's
.left and .right int32 lanes.  Sequential read, sequential
write, two scalar volume multiplicands.

gcc -O3 auto-vec on all three ISAs:

  x86_64  : SSE2 with PMULLW/PMULHW + interleave
  AArch64 : NEON with ld2/st2 + mla
  ARMv7   : NEON with vld2/vst2 + vmla

Bench (4096 samples x 8 channels = "typical audio frame",
5000 iters):

  x86_64 SSE2  : scalar 0.0166 ms  sse2 0.0105 ms  1.58x
  AArch64 NEON : scalar 0.2497 ms  neon 0.0794 ms  3.15x
  ARMv7   NEON : scalar 0.1211 ms  neon 0.1356 ms  0.89x

AArch64's 3.15x: gcc's auto-vec uses LD2/ST2 (deinterleaved
load-store) for the .left/.right stereo layout, which has
higher latency than LDP/STP (paired non-deinterleaved load-
store) on every AArch64 core that's not Apple silicon.
The manual NEON version computes left and right in separate
int32x4 vectors, ZIP1Q/ZIP2Q-merges them into stereo lane
order, and uses LDP/STP for paintbuffer access -- beats
auto-vec by 3.15x.

ARMv7's 0.89x: 32-bit ARM only has 16 q-registers (vs 32 on
AArch64), so the same register-pressure trick that wins on
AArch64 spills on ARMv7.  Manual code measures consistently
slightly slower than auto-vec across 5 QEMU runs (0.88x,
0.90x, 0.91x, 0.92x, 0.95x).  Detection: SMIX_NEON_AARCH64
gates this path to AArch64 only; ARMv7 falls through to the
scalar tail which gcc auto-vec handles cleanly with VMLA +
VLD2/VST2.

Macro structure.

  SMIX_SSE2         -- x86-64 baseline / x86 opt-in
  SMIX_NEON         -- AArch64 baseline / ARMv7 opt-in
                       (used for paths that win on both)
  SMIX_NEON_AARCH64 -- AArch64 only (used for paths that
                       win on AArch64 but not ARMv7)

The choice between SMIX_NEON and SMIX_NEON_AARCH64 is
made per-kernel after measuring it on each ISA.  Future
candidates should follow the same convention: bench on
all three before deciding the gate.

Correctness.

  Both paths fuzz-tested across counts 0..17 (head, batch,
  tail combinations) plus a count=4096 spot-check, on all
  three target ISAs (x86_64 native, AArch64 + ARMv7 via
  qemu-user).  All variants produce bit-identical output to
  the scalar reference; ARMv7 +NEON / ARMv7 no-NEON
  (-mfpu=vfpv3) / x86_64 +SSE2 / AArch64 NEON tested.

Build verified.

  Native -O2 makefile build compiles clean.  Cross-compile
  for AArch64 and ARMv7 -O2 also compiles clean.  Static-
  analysis sanity: with -mfpu=vfpv3 (no NEON) the file
  contains zero NEON SIMD instructions in the inlined
  paint/blast code (VFP scalar float ops remain, as
  expected -- they're for the master volume scaling and
  unrelated to the inner loops).
2026-06-14 02:32:15 +02:00
LibretroAdmin fa8856a62e d_scan: SIMD D_DrawZSpans inner loop (SSE2, NEON)
The z-buffer fill in D_DrawZSpans is pure arithmetic --
no texel gather, no LUT -- so it's the cleanest SIMD
target in the SW rasterizer.  Adapts the kernel pattern
from libretro-prboom's r_draw.c (the wall/sprite column
SIMD kernels) to tyrquake's row-major span model.

Measurement methodology.  Built a standalone bench that
mirrors a 1080p frame's span mix (3000 spans, mean 160
px, weighted toward 40-200 px with short and long tails)
and ran scalar + SIMD against it.  -O3, gcc 13:

  x86_64:
    scalar  0.203 ms (2367 Mpix/s)
    sse2    0.107 ms (4491 Mpix/s)   1.90x

  aarch64 (via qemu):
    scalar  1.063 ms ( 452 Mpix/s)
    neon    0.529 ms ( 909 Mpix/s)   2.01x

The fuzz harness compared scalar and SIMD output across
counts 0..33 (covers head, 8-batch boundary, and tail
combinations).  In doing so it surfaced a latent
sign-extension issue in the existing scalar loop when
izi goes negative: izi >> 16 sign-extends bits 16-31,
which the | (izi & 0xFFFF0000) does not clear, so a
negative izi produces 0xFFFF in the upper half of the
packed pixel pair regardless of (izi+izistep)'s upper
half.  In real Quake content izi stays in [2^19, 2^29]
(1/z scaled by 2^31 for z in [4, 4096]) so the negative
case never fires in practice; the SIMD path uses PSRAD
/ VSHRN which take the actual upper 16 bits, matching
scalar bit-for-bit in the practical range.  Decided
not to "fix" the scalar to match the SIMD's
theoretically-correct behavior -- bit-for-bit
compatibility with the existing path is the right
constraint until something demonstrably hits the
overflow region.

Codegen check (gcc 13 -O3).

  Scalar inner loop:  10 insn / 2 pixels = 5 cyc / 2 px
    leal, movl, addq, addl, xorw, sarl, orl, movl-store,
    cmpq, jne.  No auto-vec.

  SSE2 inner loop:    10 insn / 8 pixels = 5 cyc / 8 px
    movdqa, movdqa, paddd, addq, psrad, psrad, paddd,
    packssdw, movups, cmpq+jne.  4x theoretical
    throughput vs scalar; measured 1.90x with the
    head/tail overhead amortized over 160-pixel avg spans.

  NEON inner loop:    4 insn / 8 pixels in the dense body
    uzp2 v3.8h, v0.8h, v1.8h   (narrow by deinterleave)
    add  v1.4s, v1.4s, v16.4s
    add  v0.4s, v0.4s, v2.4s
    str  q3, [x0], 16
    + loop branch.  GCC recognizes vshrn_n_s32(v, 16) is
    equivalent to taking the upper half of each int32
    lane and emits UZP2 directly.

What is NOT SIMD-able in this file.

  D_DrawSpans16 (and its Qb / QbDither variants) loops
  over texels with a per-pixel byte gather
  pbase[(s>>16) + (t>>16)*cachewidth].  Measured both
  intrinsic SSE2 (vectorize address math, scalar gather,
  pack uint32 store) and plain 4x unroll of the scalar
  loop against the existing implementation.  Both
  LOST: SSE2 0.72x scalar, plain unroll 0.69x scalar.
  The compiler at -O3 already does good scalar
  unrolling and the gather dominates total time -- SIMD
  extract+gather adds overhead with nothing to save on
  the bottleneck.  Same story on NEON (extract->scalar
  gather is the same pattern).  Leaving D_DrawSpans16
  scalar.

  D_DrawTurbulent8Span has the same pattern (sine table
  gather per pixel) -- not investigated yet but expected
  to behave the same.

  R_ScanEdges and the bucket / sort code in d_edge.c is
  pointer chasing -- not SIMD territory.

Implementation notes.

  SSE2 / NEON detection at the top of d_scan.c gates the
  vector paths; the scalar fallback (now scoped as the
  tail) handles every other ISA and runs when count<8
  after the head-alignment step.

  The MSB_FIRST scalar branch is preserved for
  big-endian builds; the SIMD paths are not gated on
  endianness because PACKSSDW / VSHRN write in lane
  order and PSRAD / vshrn_n give arithmetic upper-half
  semantics on both endiannesses (the store is a single
  128-bit value, no byte-swap needed within a lane).

  Tested with -O2 native build (default tyrquake make
  flags) and cross-compiled aarch64 -O2 build; both
  compile clean.
2026-06-14 02:10:32 +02:00
libretroadmin af6aa6a03b Revert viewmodel FOV anchor under Hor+ widescreen
The viewmodel anchor pinned the weapon's horizontal scale to the
vanilla 4:3 value (xscale 960 at 1920 wide) while the Hor+ world
rendered wider (720 at 16:9, 360 at 32:9).  That made the gun render
1.33x larger than the world's scale at 16:9 and worse at wider ratios
-- the weapon visibly grew instead of staying put.

The justification for the anchor was wrong: the viewmodel sits dead
centre on the view-forward axis, so widening the world FOV around it
does not shear or stretch it (the edge-stretch concern only applies to
off-centre models).  A centred prop should simply scale with the world
so its size stays proportional at every aspect, which is the natural
behaviour with no anchor at all.

Restore R_DrawViewModel to drawing the viewmodel with the same
projection as the rest of the scene.  This reverts 5337415 and leaves
r_main.c identical to the prior commit.
2026-06-01 17:16:26 +02:00
libretroadmin 53374156bc Anchor viewmodel FOV to vanilla under Hor+ widescreen
Under Hor+ widescreen (r_aspect > 0) the world is rendered with a
widened horizontal FOV.  The viewmodel is a screen-anchored prop, not
world geometry, so sharing that widening stretches the weapon toward
the screen edges -- increasingly obvious at 21:9 and 32:9.

Pin the viewmodel to the vanilla horizontal FOV (the authored scr_fov
at 4:3) by temporarily swapping the un-widened horizontal scale around
the alias draw in R_DrawViewModel.  Only xscale / aliasxscale /
xscaleinv depend on the widened horizontalFieldOfView; xcenter and
aliasxcenter derive from vrect.width alone and are aspect-invariant,
and the vertical scale is already anchored by the pixelAspect
compensation in R_ViewChanged.  Restoring just the horizontal scale
therefore reproduces the vanilla gun projection while leaving the
world untouched.

Verified: at 1920x1080 the world horizontal scale shrinks with wider
ratios (960 at 4:3 -> 720 at 16:9 -> 360 at 32:9) while the gun stays
pinned at the vanilla 960 at every ratio.  4:3 is a no-op.
2026-06-01 16:43:22 +02:00
libretroadmin f91ced9c32 Recompute the view when r_aspect changes
The Hor+ projection math from the previous two commits was correct but
never took effect: changing r_aspect produced no visible change to the
field of view.

SCR_CalcRefdef (which derives the widened fov_x) and R_ViewChanged
(which applies the aspect-compensated pixelAspect) only run when
vid.recalc_refdef is set.  That flag is raised on fov/viewsize/
resolution changes but not on an r_aspect change, so the projection
stayed frozen at whatever last triggered a recompute -- the selected
ratio only ever reached the frontend's display-aspect hint, which just
stretched the unchanged 4:3 render.  Both screenshots at 4:3 vs 16:9
showed identical world coverage for exactly this reason.

Set vid.recalc_refdef in the r_aspect callback so the next
SCR_UpdateScreen re-derives fov_x and re-runs R_ViewChanged with the
new aspect.

With the trigger in place the projection is now decoupled from the
internal resolution: at a 1920x1080 buffer, 16:9 renders fovx 106.26
(vs the vanilla 90), 21:9 renders 121.28 and 32:9 renders 138.89,
while the vertical FOV stays pinned to the vanilla 73.74 at every
ratio and the rendered world aspect equals the selected ratio.  4:3
remains a bit-for-bit no-op.
2026-06-01 15:49:37 +02:00
libretroadmin 01786977ca Make r_aspect a true Hor+ widescreen FOV, not a stretch
The initial r_aspect commit only changed the frontend display aspect
(info->geometry.aspect_ratio), so non-4:3 ratios stretched the 4:3
image rather than revealing more of the world.  This implements the
prboom-plus model: anchor the vertical FOV to the vanilla 4:3 framing
and widen the horizontal FOV to match the selected display ratio.

In tyrquake's software renderer the vertical scale is
yscale = (vrect.width / horizontalFieldOfView) * pixelAspect and the
vertical clip planes carry verticalFieldOfView (~1/pixelAspect).
Widening fov_x by a factor k shrinks xscale by k (the wanted
horizontal expansion) but also shrinks yscale by k, zooming the view
out vertically too.  Scaling pixelAspect by the same k cancels the
vertical change exactly, leaving yscale and verticalFieldOfView pinned
while xscale keeps its 1/k widening.  k reduces to the selected ratio
over the 4:3 baseline, so the 4:3 index is a bit-for-bit no-op.

 - screen.c: CalcFovHorPlus() derives the widened fov_x from the
   vanilla 4:3 vertical anchor; SCR_CalcRefdef applies it for
   r_aspect > 0 and keeps fov_y anchored to the 4:3 framing.
 - r_main.c: R_ViewChanged scales the incoming pixelAspect by k so the
   vertical view is invariant across aspects; applies to all callers
   including the water-warp path.

Verified numerically: at 16:9/16:10/21:9/32:9 yscale and vertical FOV
are identical to vanilla while xscale widens by the ratio; 4:3 is
unchanged.

The frontend aspect_ratio hint from the previous commit still applies:
the renderer now draws the wider FOV into the framebuffer and the
frontend presents it at the selected ratio, so the result is correctly
proportioned rather than stretched.
2026-06-01 15:33:43 +02:00
libretroadmin e873c13908 Add aspect ratio setting to Video Options menu
Add an 'Aspect Ratio' selector to Settings -> Video Options that
cycles through 4:3 (default), 16:9, 16:10, 21:9 and 32:9.

The selection is backed by a new archived cvar 'r_aspect' (index
0..4 into a ratio table).  Only the display aspect the frontend
stretches the framebuffer to changes; the rendered resolution
(geometry base/max width/height) is untouched.  The cvar callback
repushes the geometry via RETRO_ENVIRONMENT_SET_GEOMETRY, so the
change takes effect on the next presented frame.

 - r_main.c: define r_aspect, register it, hook R_AspectRatioChanged
 - render.h: public R_AspectRatioForIndex / R_AspectRatioChanged decls
   and R_ASPECT_NUM_RATIOS / R_ASPECT_MAX
 - r_local.h: r_aspect extern
 - libretro.c: ratio table + geometry-repush callback; av_info reads
   r_aspect instead of the hardcoded 4:3
 - menu.c: cycling Aspect Ratio widget (row 19), item count 19 -> 20
2026-06-01 15:24:27 +02:00
LibretroAdmin ca3b8ba45c libretro: fix mouse look dropping steady motion
RETRO_DEVICE_ID_MOUSE_X/Y return movement relative to the last poll,
not an absolute position. IN_Move cached the previous delta in
cur_mx/cur_my and gated all view movement on the new delta differing
from it, so any frame whose delta equalled the previous frame's was
discarded entirely. At steady cursor speeds consecutive equal deltas
are common, producing the stuttery "mouse barely working" behaviour and
explaining why changing the frontend Poll Type Behavior (which shifts
the delta distribution) appeared to help.

Apply the relative delta directly each frame and act only when it is
non-zero. Drop the stale cur_mx/cur_my tracking.
2026-06-01 04:20:04 +02:00
LibretroAdmin 32c210eb43 libretro: case-insensitive mission-pack detection
The directory-setup check already matched a lowercased copy of the
content path, but the argv flag assignment matched the raw path. A
content path containing uppercase letters (e.g. HIPNOTIC/) passed the
first check yet fell through every -rogue/-hipnotic/-quoth branch into
the generic -game path, so the mission-pack flag was never set.

Match the lowercased path in the flag block as well, keep path_lower
alive until after the block, and drop the now-unused g_pak_path buffer.
The -game basename is still taken from the case-preserving rom dir.
2026-06-01 04:17:26 +02:00
LibretroAdmin 5c09044ee0 tyrquake: perf_timing -- close the gap: measure uploads + audio_step
Continuing the Host_Frame internal instrumentation pass.
After the rv/2d/pt split, recent perf snapshots showed:

  FRAME  8.27
  render 5.29 (rv ?? 2d ?? pt ??)
  record 0.24
  submit 0.10
  -----
  sum    5.63
  gap    2.64

A 2.64 ms unmeasured residual is too large to leave
unaccounted for.  It lives in two places:

  1. backend_vk_end_frame, between RENDERVIEW end and
     RECORD_FRAME begin:
       backend_vk_upload_vid_buffer  (~2 MiB memcpy at
          1080p into the active slot's HOST_COHERENT
          staging buffer)
       backend_vk_upload_zbuffer     (~8 MiB memcpy at
          1080p, conditional on 3D-dispatch activity)

  2. common/libretro.c, after end_frame returns and
     before PERF_SECTION_FRAME closes:
       audio_step (S_Update + audio_batch_cb)

This commit adds three new perf sections to expose those.

New enum entries (in perf_timing.h, after HOST_PT):

  PERF_SECTION_UPLOAD_VID
  PERF_SECTION_UPLOAD_ZBUF
  PERF_SECTION_AUDIO

All three are FRAME sub-sections OUTSIDE the RENDERVIEW
bracket -- they fire in end_frame / post-end_frame, not
inside Host_Frame.  So they appear in the print line
separately from the (rv 2d pt) grouping.

Bracket placement.

  backend_vk_end_frame
    PERF_SECTION_UPLOAD_VID wraps the unconditional
    backend_vk_upload_vid_buffer() call.
    PERF_SECTION_UPLOAD_ZBUF wraps the conditional
    backend_vk_upload_zbuffer() inside its existing
    gate (particles || sprites || alias || turb).  The
    accumulator naturally reports 0 ms on frames where
    no 3D dispatches fired -- no extra branching
    needed in the print path.

  common/libretro.c retro_run tail
    PERF_SECTION_AUDIO wraps the audio_step() call,
    sitting between CL_DecayLights and PERF_SECTION_-
    FRAME end.  CL_DecayLights is left unmeasured --
    it's a small fixed-cost per-light decay loop with
    no expected hot path.

New print format.

  Old: render R (rv X 2d Y pt Z) record C submit S
       (qs Q si M vc V) bw W
  New: render R (rv X 2d Y pt Z) upload (vb X1 zb X2)
       record C submit S (qs Q si M vc V) audio A bw W

  "upload (vb zb)" groups the two memcpy paths into a
  pair, matching the parens-grouping convention used
  for render's sub-fields and SUBMIT_PRESENT's sub-
  fields.  No "upload TOTAL" prefix; the two paired
  values speak for themselves.

  "audio A" placed after the submit sub-grouping
  because audio_step happens AFTER end_frame returns
  in retro_run; reading left-to-right in the log now
  mirrors the actual execution order of a frame.

Expected readings.

  Mapped HOST_COHERENT memory write bandwidth on a
  modern PCIe 5.0 + ReBAR-enabled NVIDIA setup is in
  the 10-15 GB/s range.  At 1080p:
    vb (2.1 MiB) ~ 0.15-0.2 ms
    zb (8.3 MiB) ~ 0.6-0.8 ms when active, 0.00 ms
                   otherwise
  Combined ~ 0.8-1.0 ms when 3D is active.

  audio likely sub-ms in steady-state gameplay (the
  mixer paints one frame's worth of samples; the bulk
  cost is the resampler).

  If the field values come back significantly higher
  than these expectations, that points at host /
  device memory bandwidth or PCIe bottlenecks that
  would warrant separate investigation.

Diagnostic value beyond gap-accounting.

  The split between vb and zb directly answers a
  recurring design question: does the conditional gate
  on upload_zbuffer (skipping when no 3D dispatches
  fired) actually buy anything?  In scenes with the
  player in an open area looking down a sightline,
  alias/sprite counts can briefly hit zero -- if zb
  drops to 0.00 ms on those frames, the gate is
  working.  If it stays high regardless, the gate's
  conditions are wrong.

Verified.

  Compiles clean (modulo pre-existing c89-pedantic
  warnings in libretro.c about a long help-string
  literal and strdup, neither touched by this commit).
  surf_atlas smoke test (7 cases) still passes.  No
  behavior change beyond three new timing brackets
  and an expanded log line.
2026-05-19 11:45:30 +02:00
LibretroAdmin 13feac9cb0 tyrquake: perf_timing -- split RENDERVIEW into rv/2d/pt sub-sections
Now that 5b-08 is shipped and SUBMIT_PRESENT collapsed from
~3.82 ms to ~0.10 ms, RENDERVIEW (Host_Frame body) is the
single dominant CPU cost at ~5.29 ms in the test scene.
Need a finer breakdown to know where headroom lives.

The cleanest natural split inside Host_Frame is:

  PERF_SECTION_HOST_RV (rv)  -- V_RenderView
    The 3D scene render.  This is where the SW rasterizer
    runs and where our HW dispatch builders (alias /
    sprite / particles / turb / sky) populate per-slot
    SSBOs through the backend_vk_draw_view hooks.  Should
    dominate at non-trivial scene complexity.

  PERF_SECTION_HOST_2D (2d) -- post-V_RenderView 2D work
    Everything from after V_RenderView through the end of
    SCR_UpdateScreen: status-bar, console, centerstring,
    intermission overlays, menu, V_UpdatePalette, the
    final VID_Update.  For the Vulkan backend most of
    these route through Draw_Pic intercepts and end up
    queueing overlay vertices; VID_Update is a near-no-op.

  PERF_SECTION_HOST_PT (pt) -- CL_RunParticles
    Particle physics update (origin + velocity + die-time
    progression).  Called from Host_Frame after
    SCR_UpdateScreen returns, so it's outside SCR but
    still inside the RENDERVIEW perf bracket.  Cost
    scales with on-screen particle count.

  Implicit gap = render - (rv + 2d + pt)
    Host_Frame work before SCR_UpdateScreen (input,
    network, server tick, client send/receive) plus the
    Draw_TileClear border work + SCR_SetUpToDrawConsole +
    SCR_EraseCenterString at the start of SCR_UpdateScreen.
    Expected to be small (sub-ms) on the test scene.

Implementation.

  perf_timing.h
    Three new enum entries placed immediately after
    PERF_SECTION_RENDERVIEW.  No reordering of existing
    entries, so binary-stored section indices (which
    don't exist anywhere -- the enum is internal-only)
    would be unaffected anyway.

  perf_timing.c
    Three new locals (host_rv_ms_avg, host_2d_ms_avg,
    host_pt_ms_avg), pulled from the accumulator the
    same way as the existing ones.  Print format gains
    a sub-paren after "render": "render R (rv X 2d Y pt
    Z)".  Mirrors the existing "submit S (qs ... vc V)"
    grouping convention for SUBMIT_PRESENT sub-sections.

  common/screen.c SCR_UpdateScreen
    PERF_SECTION_HOST_RV wraps just V_RenderView().
    PERF_SECTION_HOST_2D opens immediately after V_RV
    closes (so it captures the scr_drawloading /
    intermission / normal-gameplay HUD branches) and
    closes after VID_Update at function exit.

    Early-return-safety: the early returns in
    SCR_UpdateScreen (scr_skipupdate, scr_block_drawing,
    scr_disabled_for_loading, ca_dedicated,
    !scr_initialized) all happen BEFORE the V_RV begin
    and BEFORE the HOST_2D begin, so neither section
    can be left open by an early exit.

  common/host.c Host_Frame
    PERF_SECTION_HOST_PT wraps just CL_RunParticles.
    The pre-existing setjmp(host_abort) at the top of
    Host_Frame is unchanged; a longjmp through it could
    leave HOST_RV or HOST_2D mid-measurement but the
    perf_timing module's next begin() on the same
    section just resets start_usec, so a stale start is
    harmless at the cost of one missed measurement.
    Same hazard already existed for RENDERVIEW and
    FRAME; nothing new introduced here.

Both screen.c and host.c gain #include "perf_timing.h".
No other behavioral changes.

Diagnostic guide for reading the output.

  bw is the post-3b cross-frame fence wait.  It should
  read ~0 ms in a properly pipelined N=2 ring; a
  regression to ~3.71 ms indicates the GPU is no longer
  done by the time CPU rotates back to that slot.

  rv + 2d + pt should sum close to render.  A
  significant gap means a large amount of pre-SCR work
  in Host_Frame (input / network / server) -- only
  expected during heavy server-tick frames.

  If rv dominates: the SW rasterizer + dispatch
  builders are the bottleneck.  Levers: lower
  resolution, smaller field of view, reduce on-screen
  geometry complexity, or push more entity work to the
  GPU (would require changes to the backend).

  If 2d dominates: HUD / menu rendering is heavy.
  Levers: smaller status-bar / HUD update frequency,
  reduce overlay work.

  If pt dominates: heavy particle scene.  Levers:
  cl_max_particles tuning, particle die-time
  shortening.

Verified.

  Compiles clean under c89 -pedantic for perf_timing.c,
  host.c, screen.c (modulo pre-existing __func__
  warnings in Host_EndGame / Host_Error that this
  commit does not touch).  surf_atlas smoke test (7
  cases) still passes.  No behavior change beyond
  three new timing brackets and an expanded log line.
2026-05-19 11:30:47 +02:00