mirror of
https://github.com/libretro/tyrquake.git
synced 2026-11-07 04:35:58 -10:00
master
2232
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
e57bb11597 | Update | ||
|
|
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(). |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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). |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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
|
||
|
|
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
|
||
|
|
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
(
|
||
|
|
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.
|
||
|
|
237abee785 |
heap audit round 7: bound BSP loader count*sizeof multiplications
A class of bug adjacent to |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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).
|
||
|
|
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.
|
||
|
|
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
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|