feat: vector graphics, PSP SDI integration, and streaming video decode#51
feat: vector graphics, PSP SDI integration, and streaming video decode#51AndrewAltimit merged 29 commits intomainfrom
Conversation
Add fill_polygon, stroke_polygon, fill_arc, stroke_arc, stroke_line_dashed, and fill_polygon_gradient to SdiBackend with default implementations via triangle decomposition. Add Taylor-series sin/cos helpers (no libm, PSP-safe) and adaptive arc segmentation. SDL2 backend: scanline polygon fill, translated-coord arc rendering. WASM backend: native Canvas 2D path/arc/setLineDash calls. UE5/PSP: inherit defaults, ready for native overrides. New DrawCommand variants for batch rendering support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…it icons Phase 2 of the vector graphics layer. Creates `oasis-vector` with: - VectorOp enum (16 variants) mapping 1:1 to SdiBackend primitives - VectorScene for grouping ops with viewport dimensions - Rasterizer dispatching ops to any SdiBackend with alpha modulation - 6 Altimit-inspired icon definitions (THE WORLD, MAILER, NEWS, ACCESSORY, AUDIO, DATA) parameterized by theme colors - Background elements: wireframe sphere, grid overlay, glass polygons, radar sweep, EQ bars, active indicator - altimit_sidebar() layout helper for the full 6-icon sidebar - 30 unit tests covering all modules Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add `icon_style = "vector"` support with theme color injection: - Skin schema: `vector_preset` field in IconOverrides + IconTheme - SdiRegistry: split draw() into draw_base_layer() + draw_overlay_layer() for inserting vector content between base and overlay passes - Dashboard: "vector" icon style renders via oasis-vector directly to backend, cycling through 6 Altimit icons per app with theme colors - Vector overlay: background decorations (grid, wireframe sphere, radar sweep, glass shards) rendered between SDI passes - All three render loops updated (SDL, WASM, FFI/UE5) - New "altimit" built-in skin: .hack//SIGN-inspired dark theme with cyan/green accents and vector icon rendering - 4 new tests, 4905 total workspace tests passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add animation system for vector icons: spinning inner square (the_world), pulsing triangle alpha (audio), blinking LED (data), idle float bob (all icons), and animated wireframe sphere longitude lines. Animations are configurable per-skin via icon_overrides TOML fields and driven by the existing frame_counter. New anim module in oasis-vector provides rotation, pulse, blink, and float primitives with 8 new tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…bars, terminal Replace the PSP backend's custom direct-rendering pipeline with oasis-core's unified SDI scene graph for the main shell surfaces: - Phase 1-2: Load skin via builtin::load_builtin(), create ActiveTheme, replace dashboard rendering with DashboardState + update_sdi() - Phase 3: Replace chrome::draw_status_bar/draw_bottom_bar with SDI-based StatusBar/BottomBar for all modes, feed PSP StatusBarInfo/SystemInfo into oasis-core platform types each frame, dynamic URL text per mode - Phase 4: Replace views::draw_terminal with terminal_sdi::setup_terminal_objects, theme-driven layout/colors, blinking cursor, scrollbar overlay Remaining classic views (FileManager, PhotoViewer, MusicPlayer, Browser, Radio, TvGuide) continue direct rendering, correctly layered between wallpaper and SDI bars. Ready for PSP hardware testing. Also includes: vector icon animation fixes, screenshot collapsible_if fix, WASM debug log cleanup, altimit skin tweaks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: sceVideocodec static import stubs used flags 0x4001 (strong import), causing the PSP kernel to fail module loading when codec libraries aren't loaded at boot time. Fixed by using weak imports (0x4009) via rust-psp fix/weak-videocodec-import branch, matching sceAudiocodec's existing flags. Additional fixes: - Split SDI draw into base/overlay passes to avoid 100+ draw calls per frame in non-dashboard views (restores 60 FPS) - Make status/bottom bar backgrounds opaque on PSP to prevent darkening window content (semi-transparent alpha=80 was designed for desktop, looks muddy on 480x272) - Restore real PspVideoDecoder with lazy AV module loading (calls load_av_modules_once() before sceVideocodec init) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…cs update PSP backend: - TLS 1.3 via embedded-tls with raw TCP sockets and MT19937 PRNG entropy, enabling HTTPS on firmware with only SSL 3.0 support - In-memory MP4 streaming for TV Guide: moov atom buffering, demux_lite track parsing, sceAudiocodec AAC hardware decode - Audio backpressure: I/O thread retries with sleep when audio queue is full instead of dropping frames, throttling download to playback speed - Remove 10ms audio thread sleep during video AAC decode (was causing 31ms per frame when 21ms needed) - HttpDataSource abstraction unifying sceHttp and TLS reader paths - Enable native HTTPS URLs for TV Guide (remove http:// rewrite hack) Documentation: - Add oasis-vector crate to README, CLAUDE.md, site architecture diagram - Add Altimit skin to gallery (README, site, getting-started) - Update crate count 19→20, skin count 17→18 across all docs - Add PSP TLS 1.3 and video streaming sections to CLAUDE.md - Update PSP platform card and TV Guide feature on GitHub Pages site Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…lback sceHttp auto-followed HTTP→HTTPS redirects internally using PSP's 2008 SSL stack, causing 0x80431079 on sceHttpSendRequest. Fixed by disabling redirects on the persistent template and handling them manually. Key changes: - Persistent sceHttp template (DL_TEMPLATE_ID) with sceHttpDisableRedirect + sceHttpDisableKeepAlive -- reused across channel switches - Manual redirect following in http_open_with_redirect() with loop detection - TLS 1.3 fallback via embedded-tls when CDN requires HTTPS (redirect loop) - Non-blocking TCP connect with getpeername polling (10s timeout) for TLS -- PSP ignores SO_SNDTIMEO on blocking connect - Reliable channel switching: cleanup only deletes req+conn, template persists Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a CDN node requires HTTPS (redirect loop), the TLS fallback now tries archive.org:443 first (which may redirect to a different, reachable CDN node) before trying the CDN URL directly. This helps when a specific CDN IP has port 443 unreachable from the PSP's network. Also fixes "catalog not loaded" showing for channels with 0 episodes -- the catalog entry is now created even when empty, so the user sees "No suitable video found" instead of a misleading loading message. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three fixes for streaming reliability: 1. Drain stale VideoCmd::Stop in play_stream() — when the user presses Circle during moov buffering, the Stop sits in the queue and kills play_stream() immediately on entry. Now drained before the main loop. 2. DOWNLOAD_CANCEL atomic flag — main thread sets it on Circle press, I/O thread checks it during moov buffering (phase 1), phase 2 streaming, and TLS TCP connect polling. Prevents the I/O thread from blocking for 10s in TLS timeout while the user has moved on. 3. Cancel handler now also triggers when tv_downloading is true (not just when tv_tuned is set), so downloads can be cancelled during the moov buffering phase before streaming starts. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause of all TLS failures: psp::net::resolve_hostname used to_be_bytes() on the in_addr u32, which double-swaps on little-endian MIPS. IPs were byte-reversed (207.241.224.2 became 2.224.241.207), so raw TCP connections went to wrong hosts. sceHttp was unaffected (does its own internal DNS), which is why HTTP worked but TLS didn't. Pin rust-psp to d48345a (fix/resolve-hostname-endianness branch) which uses to_ne_bytes() to get the correct network-order bytes. Also includes: download cancellation flag, stale command drain in play_stream(), and TLS connect cancellation check. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three bugs prevented TLS from working on real PSP hardware: 1. mfc0 $9 (COP0 Count register) is privileged on PSP Allegrex and crashes in user mode. Replaced with sceKernelGetSystemTimeLow() for MT19937 PRNG seeding in all 4 call sites (getrandom v0.2/v0.3 backends, IoRng, PspRng). 2. embedded-tls without `alloc` feature doesn't advertise RSA signature schemes. archive.org uses RSA certificates, so the server rejected the handshake with HandshakeFailure. Added `alloc` to features. 3. Missing flush() after TLS write_all() -- the HTTP GET request was buffered in the TLS record layer but never sent over the wire, causing the server to hang waiting for data. Also: pin rust-psp to merged DNS endianness fix (PR #21, rev 4370415), increase I/O thread stack to 512KB for TLS crypto headroom, clean up debug logging, and update docs/site. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Split audio drain from video decode: drain ALL audio packets after each video frame instead of just one, preventing audio starvation during slow H.264 decode - Detect missing audio tracks at init and skip audio calls (fixes CH13 documentary channel silent playback) - Audio-only fallback: when H.264 fails (SkipLimit/errors), continue audio playback instead of exiting decode thread - Increase channel capacities (video 2→4, audio 8→64) and SDL queue limit (800KB→1.6MB) to reduce audio dropout from backpressure - Fix player state machine: explicit `finished` flag prevents race conditions in auto-advance; stop+cleanup before re-tune - Streaming buffer safety: 64MB cap with emergency eviction during demuxer init phase; download progress logging every 4MB - Comprehensive diagnostics: audio chunks/samples counters, periodic status reports, SDL queue drop logging, decode thread exit reasons Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Launches oasis-app with auto-tune env vars, captures logs, and reports video decode success/failure metrics. Supports channel selection, timeout, and optional xdotool interaction. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add ffmpeg-next as an alternative video decode backend (feature: `ffmpeg`), replacing symphonia+openh264 with ffmpeg's unified H.264+AAC pipeline via custom AVIO I/O for streaming sources. Statically linked -- no runtime deps. Key changes: - New `ffmpeg_decoder.rs`: custom AVIO read/seek callbacks, EAGAIN handling, sws_scale YUV→RGBA, swresample AAC→f32, buffered audio/video separation - `_video` internal feature flag so app code works with either h264 or ffmpeg - PTS-based frame pacing in video_player tick() prevents too-fast playback - `next_buffered_audio()` drains audio without advancing stream position - SDL audio device reopens on format change (mono↔stereo, sample rate) - Streaming session cancellation: old download+decoder threads abort on retune - Duplicate tune suppression: clicking same channel is ignored - HTTP Range request for moov atom: enables instant decoder open for MP4s with moov at end of file (e.g. 677MB game show episodes) - Seek-position download: parses mvhd duration, estimates byte offset, restarts download from near seek position via Range request - Applies to both moov-at-end and moov-at-start files Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ding indicator - Eliminate read_to_end() bottleneck: decoder now waits for moov data from download thread and extracts avcC from just the moov atom, skipping the expensive full-file scan that forced downloading entire episodes (100MB+) before playback could start - Add open_stream_with_avcc() to Mp4Demuxer and SoftwareVideoDecoder for pre-extracted avcC config, bypassing symphonia's full-file probe - Concurrent tail probe: spawn separate thread to fetch last 4MB via HTTP Range request for moov-at-end files, running in parallel with linear download instead of blocking it - Download throttling: cap buffer at 16MB lookahead (MAX_LOOKAHEAD) with backpressure in both download loops; throttle during init phase once moov is found, preventing unbounded memory growth - Loading indicator: animated "Loading..." text with cycling dots in PIP area while channel is buffering, with download progress percentage - Fix auto-advance texture crash: clear guide.preview_texture before destroying the texture on video EOF to prevent stale texture reference - Export find_avcc_in_mp4() and AvccConfig from oasis-video for callers to pre-extract codec config from moov data Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The symphonia ISOMP4 demuxer's probe phase calls ignore_bytes(mdat_body_size) which sequentially reads through the entire mdat body (~100MB+), blocking for 5-8 seconds on network I/O. Add a probe_mode flag to StreamingBuffer that returns zeros instantly for unretained positions during probe, then switches to real blocking reads after the decoder opens. Also fix moov-at-end files never starting a Range download -- the tail probe handler set base_offset but the main download loop didn't detect the jump and restart. Now properly detects when base_offset was moved far ahead and issues the Range request. Key changes: - probe_mode AtomicBool on StreamingInner (true during probe, false after) - disable_probe_mode() called after decoder opens in video_player - Moov-at-end restart detection in main download loop - Preserve tail-probed moov data during restart (don't overwrite with wrong-position linear download data) - Add cancellation check during Range download header reading - Add Range download response/progress logging Tested: decoder opens in 0.0s (was 5-8s), video plays at 5+ fps with audio, Range download only fetches from seek position. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…t seek, short-seek threshold Three improvements to TV Guide video streaming, inspired by studying ffmpeg's mov.c/http.c/avio.c algorithms: 1. HTTP reconnect with stall timeout: stream_download_range() now detects CDN stalls (10s no data) and reconnects with a new Range request from the last received byte, up to 3 retries. Connection logic extracted into open_range_connection() helper to avoid duplication. 2. Exact seek-byte from MP4 sample tables: parse moov atom's stts/stss/ stsc/stco/stsz tables to compute precise keyframe byte offsets instead of linear interpolation. Added parse_moov_tracks() and seek_byte_from_moov() to demux_lite (now always compiled, not gated behind no-std-demux feature). Falls back to linear interpolation when sample tables can't be parsed. 3. Short-seek read-through: named SHORT_SEEK_THRESHOLD (4MB) constant replaces magic numbers. When seek position is within threshold of downloaded data, continues linear download instead of expensive HTTP Range reconnect. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ng, stall recovery Root cause: our seek_byte_from_moov() computes video-track-only byte offsets that diverge from symphonia's coarse seek (which considers both audio and video tracks). The mismatch caused the Range download to start too far ahead, leaving a gap of zeros that corrupted the H.264 bitstream and triggered SkipLimit fallback to audio-only mode. Fix: use linear interpolation (time_fraction * file_size) as the seek estimate instead of exact sample-table offsets, since it closely matches symphonia's internal seek behavior. Back up 2MB before the estimate to ensure the demuxer finds sync points. Additional improvements: - Reduce stall timeout from 10s to 5s, increase max reconnects to 5 - Add StreamingBuffer diagnostic logging: probe_mode transition, gap-fill zeros warning (logged once per gap), seek position logging - Separate logged_gap flag from logged_wait to avoid log spam - Reset logged_wait on seek for fresh diagnostics per seek - AAC decode errors now logged with debug! instead of silently skipped - demux_lite module always compiled (removed no-std-demux feature gate) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three bugs prevented video playback on most channels: 1. **Throttle deadlock** (root cause): During probe-mode, StreamingBuffer::read() updated decoder_pos to ~1MB. After seek-restart set bytes_received to the Range offset (30-300MB), should_throttle() saw the download as 29-299MB ahead of decoder and blocked all reads indefinitely. Fixed by skipping decoder_pos updates during probe_mode reads. 2. **CDN connection throttling**: The tail probe for moov-at-end was launched eagerly before the linear stream could discover moov-at-start. Concurrent HTTPS connections to archive.org CDN caused the body Range download to receive 0 bytes. Fixed by deferring tail probe launch until >8MB downloaded without finding moov. 3. **CDN 401 errors**: Range requests sent directly to CDN nodes sometimes returned 401 Unauthorized. Fixed by routing Range requests through the original archive.org URL (which 302-redirects to a fresh CDN node) and adding redirect handling in open_range_connection. Also adds prebuffer gate (2MB minimum before seek) and fixes clippy warnings (abs_diff, !is_empty). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Update PRD, TV Guide plan, CLAUDE.md, and GitHub Pages site to reflect the implemented StreamingBuffer progressive playback architecture: - Mark Phase 2 success criteria complete (streaming, CDN failover, etc.) - Replace outdated SDL "launch browser/mpv" plan with actual implementation - Add Desktop Video Streaming section to CLAUDE.md - Document solved risks (throttle deadlock, CDN 401, connection throttling) - Update site TV Guide feature card and architecture diagram Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gemini AI Code ReviewIssues (if any)
Previous Issues (for incremental reviews)(none) Suggestions (if any)
Notes
Generated by Gemini AI (gemini-3.1-pro-preview). Supplementary to human reviews. |
Automated fix by Claude in response to Gemini/Codex review. Iteration: 1/5 Co-Authored-By: AI Review Agent <noreply@anthropic.com>
Review Response Agent (Iteration 1)Status: Changes committed, pushing... Commit: Fixed Issues
Ignored Issues
Deferred to Human
Notes
Automated summary of agent fixes. |
Gemini AI Incremental ReviewThis is an incremental review focusing on changes since the last review. Issues (if any)
Previous Issues
Suggestions (if any)
Notes
Generated by Gemini AI (gemini-3.1-pro-preview). Supplementary to human reviews. |
Review Response Agent (Iteration 2)Status: No changes needed Fixed Issues
Ignored Issues
Deferred to Human
Notes
The agent reviewed feedback but determined no code changes were required. |
Automated fix by Claude in response to pipeline failures. Failures addressed: - format - lint - test-suite Actions taken: - Ran autoformat (ruff format, cargo fmt) - Fixed remaining lint issues Iteration: 1/5 Co-Authored-By: AI Pipeline Agent <noreply@anthropic.com>
Failure Handler Agent (Iteration 1)Status: Changes committed, pushing... Commit: Failures addressed:
Automated fix in response to CI pipeline failures. |
Gemini AI Incremental ReviewThis is an incremental review focusing on changes since the last review. Issues (if any)
Previous Issues (for incremental reviews)
Suggestions (if any)(none) Notes
Generated by Gemini AI (gemini-3.1-pro-preview). Supplementary to human reviews. |
Automated fix by Claude in response to Gemini/Codex review. Iteration: 3/5 Co-Authored-By: AI Review Agent <noreply@anthropic.com>
Review Response Agent (Iteration 3)Status: Changes committed, pushing... Commit: Fixed Issues
Ignored Issues
Deferred to Human
Notes
Automated summary of agent fixes. |
Gemini AI Incremental ReviewThis is an incremental review focusing on changes since the last review. Issues (if any)
Previous Issues (for incremental reviews)
Suggestions (if any)(none) Notes
Generated by Gemini AI (gemini-3.1-pro-preview). Supplementary to human reviews. |
Review Response Agent (Iteration 4)Status: No changes needed Fixed Issues
Ignored Issues
Deferred to Human
Notes
The agent reviewed feedback but determined no code changes were required. |
Failure Handler Agent (Iteration 2)Status: No changes needed The agent analyzed the failures but no automated fixes could be applied. Manual intervention may be required. |
- Add `Self::` prefix to draw_base_layer/draw_overlay_layer doc links - Remove reference to non-existent enable_eviction method Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gemini AI Incremental ReviewThis is an incremental review focusing on changes since the last review. Issues (if any)
Previous Issues (for incremental reviews)
Suggestions (if any)(none) Notes
Generated by Gemini AI (gemini-3.1-pro-preview). Supplementary to human reviews. |
Review Response Agent (Iteration 5)Status: No changes needed Fixed Issues
Ignored Issues
Deferred to Human
Notes
The agent reviewed feedback but determined no code changes were required. |
- Fix audio-only fallback freeze: explicitly call next_audio_samples() to advance the demuxer when video_failed is true (video_player.rs) - Remove hot-path vlog calls from PspVideoDecoder::decode() that caused ~90 blocking file I/O ops/sec at 30fps (PSP video.rs) - Add redirect depth limit to TlsHttpReader::open() to prevent infinite recursion on redirect loops (PSP threading.rs) - Fix tail probe timeout: reset deadline on each successful read so slow connections aren't aborted by the fixed 30s total deadline (tv_controller.rs) - Fix infinite auto-advance loop: skip ahead when remaining_secs < 5 to avoid re-tuning to a nearly-finished episode (tv_controller.rs) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Gemini AI Incremental ReviewThis is an incremental review focusing on changes since the last review. Issues (if any)
Previous Issues (for incremental reviews)
Suggestions (if any)(none) Notes
Generated by Gemini AI (gemini-3.1-pro-preview). Supplementary to human reviews. |
Automated fix by Claude in response to pipeline failures. Failures addressed: - format - lint - test-suite Actions taken: - Ran autoformat (ruff format, cargo fmt) - Fixed remaining lint issues Iteration: 3/5 Co-Authored-By: AI Pipeline Agent <noreply@anthropic.com>
Failure Handler Agent (Iteration 3)Status: Changes committed, pushing... Commit: Failures addressed:
Automated fix in response to CI pipeline failures. |
Gemini AI Incremental ReviewThis is an incremental review focusing on changes since the last review. Issues (if any)(none) Previous Issues (for incremental reviews)
Suggestions (if any)(none) Notes
Generated by Gemini AI (gemini-3.1-pro-preview). Supplementary to human reviews. |

Summary
Major feature branch adding three interconnected capabilities across 22 commits, 72 files, ~11K lines:
Vector Graphics Layer (oasis-vector crate)
oasis-vectorcrate with resolution-independent scene graph, path-based drawing ops (16VectorOpvariants), and rasterizer dispatching to anySdiBackendSdiBackendtrait extended withfill_polygon,stroke_polygon,fill_arc,stroke_arc,stroke_line_dashed,fill_polygon_gradient-- with default triangle decomposition implementationsicon_style = "vector"in TOML,vector_presetfield, new "altimit" built-in skindraw_base_layer()+draw_overlay_layer()for vector content insertionPSP SDI Integration and TLS 1.3
builtin::load_builtin()+ActiveThemeembedded-tls(pure Rust) on 2008-era PSP firmware that only supports SSL 3.0mfc0 $9->sceKernelGetSystemTimeLow())allocfeatureto_ne_bytes()for little-endian MIPS)sceAudiocodec) for TV GuidesceHttpDisableRedirectfix for HTTP->HTTPS redirect loops0x4009) forsceVideocodec(was breaking module load on real hardware)Desktop Streaming Video Decode
StreamingBuffer-- no ffmpeg requiredStreamingBuffer:Read + Seekwrapper over shared sliding-window buffer fed by download threadprobe_modeflag: returns zeros during symphonia's probe phase, skipping mdat body instantlyshould_throttle()prevents unbounded memory growthSeekMode::Coarsevideo-decodefeature disabledffmpeg_decoder.rs: optional ffmpeg-next backend with custom AVIO I/O (feature-gated)Other
Stats
Test plan
cargo test --workspacepassescargo clippy --workspace -- -D warningspasses./scripts/build-wasm.shsucceedsGenerated with Claude Code