EVO Player v0.4.0
Pre-releasePlayback does less work per frame, and the code that does it lives in fewer
places.
The measurable part: the 4K converter stopped computing pixel addresses in
floating point, the tiling path that runs on every 1080p frame stopped
creating twelve threads to do it, and the player stopped blacking two million
pixels it was about to overwrite. The structural part: 87.7 MB of assets
nothing included are gone, icons draw from a generated table instead of three
hand-written switches, and the first module has left main.c.
Interface work too — CIRCLE asks before it stops playback, and cover art is
large enough to read as cover art.
Performance
Every change here is verified on the host by ./tools/bench.sh, which hashes
the output plane and refuses to print timings if the pixels moved. Host
timings are not the console's; they compare changes against each other.
- The 4K swizzle stopped doing a
doubledivide per pixel. The fused
converter calledpp_tiled_pixel_offsetfour times per 2×2 block, and each
call evaluated a double divide and multiply purely to compute an address —
8.3M of them per frame at 2160p. It now walks whole tile spans, so the tile
base is computed once per 512 pixels.tile_copy.chad already made this
move for the linear path and carries the proof that the integer form is
identical. 4K single-thread 38.2 ms → 20.9 ms; at 4 workers 12.4 → 10.4;
1080p at 4 workers 3.0 → 2.1. Plane hashes unchanged. - The tiling path that actually runs stopped spawning 12 threads a frame.
pp_converter_parallel.cnames per-framepthread_create/joinas the
Soft-UHD freeze root cause, and both converters were rewritten around
persistent pools because of it.pp_draw_pixels_as_tileswas missed — and it
is not dormant:pp_videoout_presentcalls it for every 1080p frame, so at
60fps it asked the scheduler for 720 thread creations a second on the render
thread. (The dormant one is the similarly namedPS5_DrawPixelsAsTilesin
main.c; they are different functions.) Now on a pool: 2.00 ms → 1.68 ms
on the host, and the console is where the stalls were actually observed. - The player no longer blacks 2M pixels it is about to overwrite.
draw_player_screencleared the whole frame and then had every pixel of it
replaced by the video on the next line. The clear could not simply be
deleted — it is load-bearing when no frame is ready, and the render thread
cannot ask "is one ready?" first, because the seek thread can retire the
display between the question and the answer. So the guarantee moved down:
pp_playback_copy_displaynow leaves every pixel of its target defined,
decided under the lock that owns the display. Worth 0.28 ms of a 2.49 ms
render thread — real, and smaller than it was billed as. - The full-frame display copy is one
memcpyrather than 1080 row-sized
ones, which is what it had always been in the case that runs every frame. - Glyph lookup is a table, not a linear scan of the 69-character
RR_CHARSrun once per character drawn — and the miss case, which is most
punctuation in a filename, paid all 69 comparisons. There were two hand-
copied implementations of that scan; there is now one function.
Changed
-
The thumbnail worker is its own translation unit.
main.cwas ~18k
lines in one file, and the received wisdom was to pull decode/demux out
first. Measuring the file says otherwise: half its 263 file-scope globals
are referenced across spans of more than 5,000 lines, and the most smeared
of them are the playback ones —g_pp_pbis touched across 17,899 lines,
player_pausedacross 17,311. Cutting there first would publish sixty
globals through a header and turn a tangle into a tangle with an API.The scrub-preview thumbnail worker measured as the real seam: 1,096 lines,
39 symbols, and a total dependency on the rest of the program of two helpers
and two screen constants. It owns its thread, mutex, condition variable and
request queue, so the boundary already existed — it just wasn't a file. It
is nowmedia/src/prospero_thumbnail.cbehind a six-function header.
main.cis 16,815 lines and 199 file-scope statics, from 17,985 and 229.Nothing about the code changed: the move is checked mechanically against the
original region, and differs only by the four substitutions its header
documents. -
Source-over pixel blending is in one header.
main.ccarried two copies
of it, the second commented "duplicated rather than shared … because that one
is defined ~3000 lines further down" — a workaround for declaration order
inside a single translation unit, which is exactly the problem a header does
not have. Both are nowevo_blend_pixel()inui/include/evo_blend.h, with
identical arithmetic to what they both already did. -
Icon drawing goes through a generated table.
tools/gen_icons.pynow
emitsEVO_ICON_TABLE/EVO_CTRL_TABLE, and both the player and
tools/uiview.cindex them. There were three hand-writtenswitch (idx)
copies naming the same macros — and one had already drifted:rr_icon()in
main.c stopped at case 12, so it drew nothing for HOME or LOGO. It is
reachable throughEVO_DRAW_VTABLE, so that was a defect waiting for its
first caller rather than a live one. Adding an icon is now one edit to the
generator; it can no longer land in the mock and be forgotten in the player. -
87.7 MB of unreferenced assets are gone —
approved_raw_assets.h,
approved_ui_assets.h,pp_ui_assets.h,pp2_ui_assets.h,
pp_font_metrics.hand three.bmps. Nothing included any of them; the
asset directory went from 117 MB to 30 MB.pp_font_metrics.hgoing with
them settles the "two font systems exist" warning in the handoff doc — there
was one, plus a header nothing read.
New
- CIRCLE during playback asks before it stops. It was the one
irreversible thing CIRCLE did anywhere in the app — tearing down the
decoder, the audio port and your position in the file — on a single press
of the button that means "back" everywhere else. It now opens a prompt over
the paused frame: CIRCLE (the button that opened it) keeps watching, CROSS
stops. The action order is deliberate: the destructive answer is not the one
you reach by repeating what you just did. - Cover art is large enough to be cover art. The cache moved from 80×80 to
320×180 and the recent shelf draws it full bleed instead of as an inset
thumbnail, so tiles are posters. 320×180 is 16:9 — the shape the source
frames actually are — and it minifies into the tile's 270×162 drawing
area rather than magnifying, which is what the old 3.4× upscale could never
do. The cache is 16 slots rather than 48: only the eight recent tiles ever
ask for one, and the trade is 3.7 MB against 1.2 MB. - The application mark is the EVO logo. The top-left of the rail and of
the launch header drew two concentric circles standing in for a logo. Both
now drawEVO_ICON_LOGO— the ring-and-play mark from the home-screen icon,
generated bytools/gen_icons.pyas one monochrome glyph so it tints with
the theme like every other icon. - Sound and lightbar survive a relaunch. Appended to the settings file
after the theme name, and written when the Tools rows are toggled.
Fixed
- Tools' DEBUG OVERLAY toggle did not save, though Settings' DEVELOPER
MODE row — which flips the same variable — always had. Which page you used
decided whether it stuck. - Full-bleed tile artwork is cropped, not stretched. Scaling a 16:9 cover
into a 5:3 tile is a ~7% horizontal squash: invisible on a gradient,
obvious on a face.evo_widget_tilenow takes the centred source rectangle
that fills the tile, the same way the hero backdrop already did. - LEFT no longer opens the navigation rail over the subtitle picker.
Neither it nor the new stop prompt was listed inevo_screen_is_modal(), so
the rail could open as a third layer over a panel floating over video. - The 4K-surface dance around a modal overlay is written once. Media Info
and the stop prompt both have to leave a 4K plane and hold the presentation
clock; that was ~25 lines transcribed by hand, and a second copy is exactly
how a 4K session comes back from an overlay stuck in 1080. Both go through
pp_product_overlay_enter()/_leave()now.
Tooling
tools/uiplay.shcovers the new prompt (Mcycles to it), and the fixture
artwork is 320×180 with a ring drawn in it — a gradient could not show
whether a cover had been squashed, and 128×72 magnified into everything it
was drawn on. The hero gets its own 960×540 fixture, matching
EVO_HERO_ART_W/_H, so the mock stops rendering stair-stepping the console
never produces.
Install
- Jailbreak the console (12.70) and start
ps5-payload-elfldr. - Deploy ps5-payload-websrv.
- Extract the homebrew zip to a USB stick as
/homebrew/EVOPlayer/,
or copy it to/data/homebrew/EVOPlayer/over FTP. - Launch it from
http://<ps5>:8080/index.html.
EVOPlayer-*.elf is the bare payload. Note that sending it with
elfldr alone runs it headless — it needs the homebrew launcher to
get a display plane. See the README.
Requirements
- PS5 on firmware 12.70, jailbroken (re-run after every reboot)
- A USB stick for media
Verification
Built reproducibly in CI from the pinned toolchain (clang 18.1.3,
PS5 Payload SDK v0.42, FFmpeg 7.0.1). CI has no console, so these
binaries are not hardware-tested. Checksums in SHA256SUMS.txt.
Licensed GPL-3.0-or-later. Derived from ProsperoPlayer; see NOTICE.