With [timing] guest_cycle_scale active, every one-cycle psx_cyc_charge went
straight to psx_cpu_charge and psx_advance_cycles, bypassing the batch the
scale-1 path uses. [timing] guest_cycle_scale_batch (PSX_GCS_BATCH=0/1)
scales the charge in place, with the same fractional carry and order as
psx_cpu_charge, then batches it like the scale-1 path. It flushes at the same
observation points. Default off.
R4 exactness, frame_fingerprint off vs on, 9000 frames each: headless boot
through intro, title and attract; a 1P Grand Prix race; and a 2P VS race.
cyc, mmio, mc and qc are identical on every frame. sp, wr, pc and ws are
identical at the end. The only differences are single-frame VBlank straddles
of wc/sc (never two frames in a row). 0 dispatch misses.
Speed, release builds, races at 60 Hz:
- M4 (macOS sample): psx_cpu_charge 4.3 % -> 0 % of emulation-thread samples;
busy 46.0 -> 44.4 % (2 pairs).
- 9800X3D: no measurable change (30.0 vs 30.5 %; one core 44.6 vs 44.2 %).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Smooth motion lost most of its generated frames with bloom on (R4 race,
120 Hz: ~30% of bloom-off). Measured and fixed in order of cost:
- Depth-stencil blits (CPU milliseconds each on macOS) are gone:
generation surfaces no longer copy the marks and are not marked while
drawn (generated frames reuse the newer real frame's mask); the
native-wide centre copy is colour only again and the mask takes the
centre's marks from the hr surface; the mask itself is resolved in the
presented surface's own pixels through an R8 target that shares the
surface's depth-stencil renderbuffer, then mapped to the window.
- Marking needs one pass per batch, not two: 3D batches only set, 2D
opaque ones only clear, semi-transparent ones only set; only batches of
unknown make (queued replays, lines) still do both.
- No full-size copy of the image: `source_down` (a blit straight to the
pass's size) feeds the bright pass, and the composite screen-blends onto
the window (`blend screen`; unlit pixels are discarded, so they keep their
exact value). The debug split/bloom-only views go with the copy.
M4, R4 race, render thread + reprojection, PSX_FRAME_GEN_FORCE, a host
loaded by other work (absolute rates low): bloom pass on real / generated
frames 1.2 / 0.7 ms at 1080p, 1.3 / 0.9 ms at 1440p, 2.6 / 1.6 ms at 4K
(glFinish-bracketed); generated frames with bloom 79-80% of bloom-off at
all three (was ~30%). A 1/8-resolution blur chain was tried and kept no
more frames, so the 1/4 chain (the look) stays.
Tests: parser (blend, source_down, bloom preset reads no full copy), GL
fixture (source_down + blend screen chain exact; HUD and mask checks
unchanged), FXAA unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #584: the default must not make guest-visible VRAM depend on the
internal scale. Mode 1 ("on", default; "scaled" kept as an alias) now
puts the PS1 pattern on the native VRAM grid; mode 2 ("fine") is the
elective per-internal-pixel pattern. gl_dithering_test checks the default
mode's native VRAM is identical at 4x and 1x (equal-colour Gouraud now
averages 15.50 in the default at every scale). Docs keep the
semi-transparent approximation explicit. OpenBIOS stamp regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Maintainer review of #584: PS1 dithering is hardware behaviour, so it is
now on by default whenever the guest sets GP0(E1h) bit 9 ([video]
dithering = "on"; "scaled" stays opt-in, "off" is the old true-colour
output).
Eligibility is decided per primitive from its command and carried in the
vertex instead of being inferred from the colour:
- Gouraud polygons and shaded lines: geometry vertex a_col.a + 2*131072
(decoded in GEO_VS, v_dith);
- texture-modulated polygons: a_semi + 8 (v_dith in TEX_VS);
- flat polygons/lines, raw-texture polygons and every rectangle: never.
This fixes the two reproduced bugs (Gouraud with 5-bit-exact colours was
skipped; textured rectangles were dithered). Bit changes no longer flush:
eligibility is fixed when the primitive is queued; mode changes still drain.
gl_dithering_test covers each case (Gouraud incl. equal colours, shaded
line, modulated and neutral-modulated textured polys dithered; flat tri,
flat rect, flat line, raw poly, modulated and raw textured rects never;
bit clear and off unchanged), plus scaled 4x == 1x, render thread, PGXP
depth, native-wide and the accurate-blending basis. OpenBIOS stamp
regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The OpenGL renderer has always drawn shaded and texture-modulated pixels
at 8 bits per channel with no dither pattern (true colour). That stays the
default. 'on' applies the PS1 4x4 offset matrix and 5-bit truncation per
internal-resolution pixel; 'scaled' puts the pattern on the native grid,
so it reads like the console at any scale and the native VRAM is the same
at every internal resolution.
GP0(E1h) bit 9 reaches the backend through a new optional facade hook
(gr_set_dither) recorded for the render thread and Smooth motion redraws.
With the mode off nothing flushes and no uniform changes. Flat, raw and
other 5-bit-exact pixels are never touched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Uniform locations are looked up once per program (each lookup can be a
driver round trip on a threaded GL; the executor runs per present,
generated frames included).
- State save queries only capability flags and the viewport. The mask pass
no longer clears (it draws 0 then 1), so the clear colour is never
touched; the present's window target and texture unit 0 are left bound.
The GL fixture checks exactly that contract.
- postfx_chain_test: a preset with CRLF line ends parses the same, and the
import check writes its shader in binary mode (it failed on Windows).
- docs/POST_EFFECTS.md: `generated reuse`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Post effects now run as chains of full-screen fragment passes on one
executor (gl_postfx.inc), as asked in review. A chain is a small .pfx preset
(postfx_chain.c, GL-free): passes with a shader (a file next to the preset or
builtin:<name>), inputs (source, the optional primitive-derived mask,
previous or a named earlier pass), a scale, format, filter and uniforms.
The executor owns target allocation and lifetime, program build/cache,
uniform binding (u_rcp, u_size, u_org) and GL state save/restore.
Stages, in order, each from a fresh copy of the image: the built-in "bloom"
chain ([video] bloom), a user chain ([video] post_shader / PSX_POST_SHADER,
elective, off by default), and the built-in "fxaa"/"fxaa_hq" chain
([video] antialiasing_mode). FXAA folds in as a one-pass chain and its
output is byte-identical to the former fixed pass; it still runs on real
frames only. Generated (Smooth motion) frames run bloom and the user chain.
The "may glow" mask is an optional pipeline input: draws keep stencil bit 1
only while an active chain reads `mask`, and the window-sized mask texture is
built once per present for the chains that use it.
Tests: postfx_chain_test (format, built-ins, errors, file import, no GL);
gl_bloom_test gains an imported two-pass chain (byte-exact invert), the mask
input (HUD unlit, 3D lit), a bad preset, a pass that does not compile, and GL
state restored with bloom + a mask chain + FXAA on, all on the caller thread
and the render thread. docs/POST_EFFECTS.md documents the format.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[video] bloom = 0.0 (off, default) .. 2.0 adds a glow around lights on the
OpenGL backend. PSX_BLOOM overrides it for one run; {"cmd":"bloom"} reads or
changes strength, threshold and a debug view live. It is a plain [video] key,
so [quality.*] presets can switch it off on low-end machines.
The HUD never glows: with bloom on every textured and untextured draw is
followed by two stencil-only passes of the same vertices that keep a
"may glow" mark in stencil bit 1 (bit 0 stays the mask-bit mirror). Opaque
primitives clear it under their coverage, then PGXP-proven 3D primitives and
additive semi-transparent texels/primitives (modes 1 and 3) set it, painter
ordered like the colour; 3D and non-3D primitives never share a batch while
bloom is on. Semi-transparent primitives leave the mark alone and the PGXP
depth-clear pass (colour writes off) marks nothing. Without PGXP geometry only
additive primitives glow.
The pass runs on the composed game image before AA and the OSD: the presented
surface's marks are blitted into a quarter-resolution stencil, a soft-knee
bright pass runs under it, three separable Gaussian iterations spread it and a
screen blend adds it back. The marks follow the picture: native-wide centre
copies and Smooth motion surfaces (redraw and reprojection) copy stencil with
colour, and generated frames get the same pass. Off: nothing runs, no state
changes, and the composed frame is byte-identical to master.
test_gl_bloom (gl_bloom_test): off is byte-identical before/after bloom and
with --dump against master; on, a bright 2D panel and a panel over a lit 3D
polygon are byte-identical to off, an additive and a 3D polygon glow; guest
VRAM is unchanged; on this thread and on the render thread.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review (mstan): a host-output callback could change the shared CPU (GPR2 set
to 0x12345678 survived an online psx_mod_function_entry). Each host-output
callback now gets a private CPUState copy whose changes are discarded, and
while one runs (psx_mod_host_output_scope) every guest-mutating mod service
refuses: memory writes, host_write_ram, call_guest*, DMA / SPU / VRAM
uploads, guest and GPU-DMA allocations, texture packet memory, the
cycle-scale gate and finish_function. netplay_view_mods_test reproduces the
review case (GPR2 + PC changed in the callback) and checks the shared CPU is
byte-identical after the hook; it fails without the copy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A plugin that only reads guest state to drive this player's host output
(rumble, haptics) can now declare netplay = "host_output". Online it stays
on per player: its function-entry hooks run in the shared simulation while
the rest of the own-view plan is parked, a completion request is ignored and
every psx_mod_write_* it attempts is refused, so it can never diverge peers.
Offline it is an ordinary plugin. netplay_view_mods_test covers both.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NVIDIA quantises the constant blend colour to the 8-bit target (0.5 ->
127/255), putting B/2+F/2 and B+F/4 one step under a k*8 boundary, so
gl_blend_accuracy_test failed there (2016 mismatches). With accurate
blending on, use 128/255 and 64/255: exact in unorm8, and the small upward
bias stays inside the PS1 truncation bucket for every 5-bit operand, so
fixed-point and float blenders agree. Passes on Apple M-series GL and an
RTX 4080 SUPER (NVIDIA 610.88). Default path unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Verifies the four PS1 semi-transparency modes, set-mask, mask check and
texel STP at 4x internal resolution with PGXP depth on and off, directly
and on the render thread, against the PS1 formulas and the software
rasterizer (new gl_blend_accuracy_test).
Finding: with the historical surface bases (fills and texels stored as
k*255/31, everything else as k*8) single-layer blends are off by one 5-bit
step in places (1312 of the test's checked pixels, e.g. B+F/4 over a fill).
[video] accurate_blending = true stores every VRAM write as k*8 so GL's
8-bit blend truncated by the readback is exactly the PS1 arithmetic; whites
of fills/texels then present as 248 like uploads already do. Default off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Filter mode 3 magnifies textures with an edge-directed rule after Hyllian's
xBR (written from the published description, no code reused): for the
texel corner nearest a sample, weighted YUV colour distances over a 5x5
neighbourhood decide whether an edge runs along the corner's diagonal; if
so the corner is cut with an anti-aliased boundary and takes the closer
side neighbour. Flat areas, straight edges, gradients, cutouts and 1:1 draws
stay nearest. Selectable from [video] texture_filtering, settings.toml,
PSX_TEXTURE_FILTER and the new texture_filter TCP command. Default unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Enhancement arenas (GPU DMA aperture, Expansion-1 mod memory) were untracked, so Crash widescreen drew every vertex native with no depth. Adds depth stats/clear causes to pgxp, depth to pgxp_tri_ring, input_route_start at_frame, integer number keys in game.toml.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Some titles compute vibration for every pad but only send it over SIO to a
DualShock (R4 with a presented NeGcon). A trusted mod can now hand the runtime
DualShock motor values per seat; the host pad gets the louder of that and the
guest's SIO motors. Values lapse 8 VBlanks after the last call and are cleared
at every session reset, so nothing can stick on. Opt-in: no title that does
not call it changes. PSX_RUMBLE_TRACE now also logs headless.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[video] fmv_chroma_smoothing = true makes the OpenGL present of a 24-bit
(MDEC) frame keep each sample's luma and take its chroma from a [1 2 1]
tent over the 3x3 source pixels, hiding the 2x2 colour blocks of 4:2:0
video without softening luma. Pairs with the existing fmv_filter upscale.
Guest VRAM/MDEC output untouched; 15-bit presents and the default unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[video] texture_lod = "mipmap" emulates mip levels for PS1 texture pages,
which are decoded per fetch from VRAM and so have no GL mip chain: the
screen-space UV footprint of a minified, proven-3D primitive is box-averaged
(opaque texels of the fragment's STP class only), and anisotropic_filtering
(1..16) spreads that many taps along the footprint's major axis. Every tap
clamps to the primitive's UV bounds or texture window, so packed atlases
never bleed. The mode rides in the textured batch's filter key; off (the
default), sprites/HUD and magnified textures are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>