Add selective multi-mip tiled decoding, 16K tile support, per-frame RC, and codec callbacks#228
Conversation
Replays the TMV feature branch (tmv_tiled_output, 51 commits since the AcademySoftwareFoundation#139 fork point) on top of AcademySoftwareFoundation/openapv main, which had diverged with a codebase-wide API rename and input-validation hardening. The TMV line had been developed on a re-initialized git history with no shared ancestry, so this is a port rather than a merge. Conflict resolution highlights: - Adopt upstream's renamed encoder/decoder API (num_c, c_sft, fn_blk_from_pic/fn_blk_to_pic, oapv_validate_tile_topology, 4-arg args_set_variable_by_key_long, 5-arg oapvd_vlc_tile_header, 3-arg oapv_bsr_read_direct, 3-arg dec_frm_prepare) throughout the TMV-added code. - Keep TMV features: dynamic tile arrays (encoder + decoder), per-frame RC (rc_param_frm), fn_imgb_to_blk_rc sampler, selective/disk-I/O multi-mip decoder + tile-status enum + tile buffer structs, --tmv-mips encoder option, per-AU output files, 64x64 tile limits. - Keep upstream hardening: ndata[0] bitstream-overflow flag path, fps/band validation, payload-bounds checks, 16-bit imgb_write range. - Keep TMV's total_bits u64 overflow fix (16k rate control). - Recognize .oapv (not just .apv) as a single-file output extension so the per-AU split path only triggers for extension-less output names. - Add dec_frm_prepare_selective for the selective/multi-mip decode paths: upstream's hardened dec_frm_prepare validates the imgb against frame dimensions/format/capacity, which rejected the dummy imgb those paths pass (they decode into their own work buffers). The selective variant sets up ctx metadata + tile geometry without imgb validation, leaving upstream's dec_frm_prepare intact for the normal full-frame path. Builds clean (gcc 13, all targets). Encode/decode roundtrip validated (125-frame 320x240 422p10). All 16 ctest tests pass. Selective multi-mip decode API (oapvd_decode_selective_multi_mips) validated via decoder_test run_4k_tests set: all real cases decode correctly; the only non-OK results are the intentional negative tests (non-existent mip -> NOT_FOUND) and a sparse-corner layout that trips the validator's zero-column heuristic. Original 51-commit TMV history preserved in branch pr_candidate_backup. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
These two public selective-decode entry points are unused by the Unreal integration, which relies solely on oapvd_decode_selective_multi_mips (oapvd_decode_selective_multi was itself just a single-mip wrapper around it). Removing them and their now-dead internal helpers: - oapvd_decode_selective (standalone disk-I/O tile-cache decoder) - oapvd_decode_selective_multi (single-mip wrapper) - exclusive helpers: oapvd_build_tile_cache, calculate_tile_offsets, oapvd_get_tile_offset, oapvd_invalidate_tile_cache (+ its call in _multi_mips), and the tile_offsets_cache fields in oapvd_ctx / their free block in dec_flush - already-dead orphans in the same subsystem: oapvd_locate_mip_frame, dec_tile_to_views, create_tile_views, init_tile_buffer_manager, get_tile_buffer, return_tile_buffer Tests that drove the removed APIs (decoder_test single/multi cases, the per-mip perf-comparison baseline, and the sequential baseline in test_multi_mip_performance) are repointed to a small local wrapper that calls oapvd_decode_selective_multi_mips with a single-mip request -- the exact behavior the removed _multi provided -- so coverage is preserved. Net: -715/+74 lines in src, no API behavior change for the retained path. Builds clean; decoder_test (single/multi/multimip) and both dedicated multi-mip test executables pass; single-tile output verified bit-for-bit correct (perfect chroma) through the retained API. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With oapvd_decode_selective / _multi removed, oapv_selective_decode_t had no remaining public consumer -- only the test-local single-mip wrappers still referenced it. Remove the type from the public header and rework the two test wrappers (decoder_test.c, test_multi_mip_performance.c) to operate directly on oapv_mip_request_t: the helper now wraps the caller's mip_request as a one-entry multi-mip decode, and status/frame metadata are filled in place (frame_width_mb_aligned / frame_height_mb_aligned / tile_width_mb_aligned / tile_height_mb_aligned) instead of being copied back into a separate struct. Builds clean; decoder_test (30/30 applicable cases), both dedicated multi-mip test executables, and ctest (16/16) all pass; single-tile output still bit-for-bit correct through oapvd_decode_selective_multi_mips. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BSW_FLUSH_4BYTE is only reachable via BSW_WRITE_32BITS, which is unused in both the TMV work and upstream (the encoder VLC path writes exclusively through BSW_WRITE_64BITS -> BSW_FLUSH_8BYTE). During the port the TMV ndata[0] overflow variant of BSW_FLUSH_4BYTE was kept, leaving it inconsistent with BSW_FLUSH_8BYTE and divergent from upstream for no functional benefit (the macro is dead code). Restore upstream's BSW_FLUSH_4BYTE verbatim (advance cur past end on overflow, same style as BSW_FLUSH_8BYTE). The live 64-bit path is unchanged and still detects overflow via the post-encode size check and oapv_bsw_sink() setting ndata[0]. Encoder VLC flush macros are now byte-identical to upstream. Encode/decode roundtrip (125 frames), decoder_test, and ctest (16/16) pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 64-bit bitstream writer (upstream AcademySoftwareFoundation#184) tracks free bits in a 64-bit code accumulator, so leftbits must reset to 64. Two TMV-added graceful overflow branches still reset it to 32 (stale from the pre-64-bit code): - bsw_flush(): overflow branch - oapv_bsw_write1(): buffer-full branch Both sit on the ndata[0] error path, but leftbits = 32 is inconsistent with the 64-bit accumulator and makes BSW_GET_SINK_BYTE ((64-leftbits+7)>>3) report 4 phantom pending bytes instead of 0 after the code register was zeroed. The normal paths in both functions (and oapv_bsw_init/_sink) already use 64. Upstream's oapv_bs.c uses 64 exclusively. (The remaining leftbits = 32 in oapv_vlc.c is inside BSW_FLUSH_4BYTE and matches upstream verbatim -- a genuine 4-byte flush, left as-is.) Encode/decode roundtrip (125 frames) and ctest (16/16) pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The multi-mip istream parser assembles three 32-bit big-endian fields (au_size, signature, pbu_size) from u8[4] buffers. `buf[0] << 24` promotes the u8 to int, and for byte values >= 0x80 the result exceeds INT_MAX -> signed integer overflow (undefined behavior; works on mainstream compilers but flagged by UBSan / risky under aggressive optimization). Cast each byte to u32 before shifting. Not a code-accumulator issue (the 64-bit bitstream register class came back clean in a full sweep of the TMV-added writer/reader code); this is the same "shift assuming 32-bit int" family in the disk-I/O format parse. Selective decode, error-handling test, roundtrip (125 frames), and ctest (16/16) all pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This standalone multi-mip test has no build target in CMakeLists.txt (the built test programs are decoder_test, test_multi_mip_performance, and test_multi_mip_error_handling), so it is never compiled or run. Its coverage -- a single-mip and a multi-mip oapvd_decode_selective_multi_mips call with the two-phase metadata/decode pattern -- is fully subsumed by decoder_test's multimip_* cases and the two dedicated multi-mip test executables. Removing the dead, superseded source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The TMV fork hardcoded fh->tile_size_present_in_fh_flag = 1 in oapve_set_frame_header, unconditionally changing the emitted bitstream so the selective/tiled decoder could index tiles from the frame header. That can't ship upstream, where the flag defaults to 0. Make it a proper encoder parameter: - oapve_param_t gains tile_size_present_in_fh_flag (public API), defaulted to 0 in oapve_param_default (matches the base bitstream / upstream). - oapve_set_frame_header now copies it from param instead of forcing 1. - The encoder app couples it to --tmv-mips: that workflow produces per-AU tiled output intended for selective/tiled decoding, which needs per-tile sizes in the frame header. Without --tmv-mips the flag stays 0. Callers that need indexable output (e.g. the Unreal integration) set param.tile_size_present_in_fh_flag = 1 via the API directly. Validated: default encode is base bitstream (roundtrip 125 frames, ctest 16/16); --tmv-mips encode produces decodable per-AU tiled output; selective decode on existing tiled media unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The full-validation chroma check took the bounding box of all non-zero luma and flagged any chroma column that was entirely zero across that box. For sparse / partial tile selections (e.g. 16k_tile_limit_validation decodes only 4 corner tiles + center) the bounding box spans the whole frame while ~95% of columns are legitimately undecoded gaps, so the check reported spurious "Chroma stripe artifacts detected" despite a correct decode (verified visually: the decoded tiles are clean, the rest is zero-filled). Gate the check on the luma plane: only evaluate rows where the co-located luma sample is non-zero, and only count columns that have any decoded luma content. A column is flagged only when chroma is zero across the rows where luma exists -- the actual "chroma dropped where luma present" defect the check is meant to catch. Undecoded gaps no longer count. Result: 16k_tile_limit_validation, multimip_sparse, and the other sparse cases now report SUCCESS; contiguous decodes (e.g. single_tile_mip0_origin) still validate exactly as before. Zero false warnings across the 4K suite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
run_multimips_tests.bat was almost entirely redundant: 5 of its 6 cases (multi_all_mip9, multimip_single_center, multimip_2x2_blocks, multimip_sparse, multimip_performance_comparison) were already run by run_4k_tests.bat against the same media. Only invalid_test was unique. - Add invalid_test to run_4k_tests.bat's multi-mip section. - Extend PNG conversion to also process output\*.y4m (the multi-mip tests emit Y4M; run_4k previously converted only .raw, so those outputs were never visualized) using convert_y4m_to_png.py with the tile converter as fallback -- the capability run_multimips provided. - Remove the now-fully-subsumed run_multimips_tests.bat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The generation script only ever uses the first 10 frames (4K path, NUMFRAMES=10) and one more frame for the 16K path. The 16K tests only exercise tile addressing / chroma at 16K resolution, so the source frame is arbitrary -- repoint the 16K path from the 9.22s frame to frame 0 so the whole clip can be trimmed to 10 frames. - koala.mp4: re-encoded to the first 10 frames (h264, crf 12), 75MB -> 5.9MB (under GitHub's 50MB limit, no LFS needed). - generate_koala_tiled.bat: 16K extraction now takes frame 0; header/comments updated. Verified end-to-end from the trimmed source: 10-frame YUV extract -> 4K --tmv-mips encode (10 frames) -> decode (primary + 7 mips); 16K frame-0 extraction works. Committed .apv1 test media is unchanged and still valid. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes the reviewer's findings on the selective multi-mip decode path: - Thread race: spawn num_threads-1 workers so the main thread's core (core[num_threads-1]) is no longer shared with a worker (matches the regular decode path). Sharing corrupted coef/prev_dc/q_mat state. - Double-free: drop the duplicate oapv_mfree(mip_contexts) in the mip_infos OOM path. - I/O hardening: check every istream->read() return; on a short read flag the coalesced block, mark its tiles ERROR so workers skip them (never decoding a partly-filled buffer), and return OAPV_ERR_MALFORMED_BITSTREAM. Validate fh->tile_size[] against the PBU payload bounds before computing offsets. No calloc on the hot path (would memset multi-MB blocks needlessly). - Infinite loop: oapvd_parse_frame_headers no longer spins when the header buffer cannot grow (clamped at 64 KB with pbu_size > 64 KB); the two grow-and-retry blocks are merged and bail on no progress. - Missing tile_size flag: hard-fail a mip with OAPV_ERR_UNSUPPORTED when tile_size_present_in_fh_flag is absent, instead of computing offsets from a 65536 fallback. - log_msg: vsprintf -> vsnprintf; default verbosity WARNING (was DEBUG, which printed the perf summary on every decode). - Return code: per-mip outcomes go only to mip_req->status; the call returns OAPV_OK unless a fatal (shared) error occurs. - Dangling pointer: clear ctx->imgb after dec_frm_prepare_selective. - NULL-safe setters: oapv_set_memory_callbacks / _cputrace_callbacks now reset to defaults on NULL; usage contract documented in oapv.h. - Dead code: remove oapv_tile_data_t, oapv_tile_buffer_mgr_t, the unused tiles_ready/io_complete worker fields, and the commented ON_DECODING block. ctest 16/16, error-handling + perf tests, and 68/68 decoder_test 4K cases pass; truncated-file decode returns -202 cleanly at 1/4/32 threads. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prepares the library/app-only PR branch: removes the TMV test harness (decoder_test, multi-mip perf/error tests, convert_*.py, run_*.bat, framelist), all bundled test media (koala.mp4, koala_tiled/*.apv1, AlphaTransition), the decoder_test/test_multi_mip CMake targets, the Windows build.bat helper, and the doc/draft-lim-apv-07.txt spec draft. Leaves only the library (src/, inc/) and app/ changes plus upstream's own test fixtures. The full with-tests reference remains on pr_candidate; the test suite continues on its own branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Reference presentation: https://youtu.be/qs6wo67YzME?si=NKcoVqIwD2s_6xpd |
|
This is kind of super patch. |
Proposal: 16k tile supportTile grid limits raised to 64x64 (OAPV_MAX_TILE_ROWS/OAPV_MAX_TILE_COLS), supporting 16K at 256x256 tiles. Tile arrays are allocated dynamically rather than fixed. DiscussionThe max tile rows and max tile coloums are defined in section 9.4.1 of RFC 9984; Therefore, oapv.h defines the following values; Basically, it is kind of 'spec violation' if the max values are changed. |
Proposal: CallbacksCodec callbacks DiscussionCould explain main purposes of the callbacks? |
Proposal: Rate control per-frameRC parameters can vary per frame within a sequence (rc_param_frm[]), loaded per frame during encode. DiscussionIn order to support indivisual multi-frames rate controlling, the |
Tile-based decodingDiscussionThere is new API to support partial decoding based on tile position; Have you checked the function can supports the requested feature? |
I agree that the rate control model should be managed on a per-frame basis. However, in order to support multiple frames within a single AU (Access Unit), further changes seem to be required. Could you please clarify if your goal is to encode multiple frames within a single AU? |
Yes, it is the goal. In particular for TMV use case, we encode mip 0 as the primary full resolution frame and the lower resolution mips (1, 2, 3, ... N) as non-primary frames in the same AU. For interop: Non-TMV compliant players ignore non-primary frames and can still play the sequence normally (although only confirmed with ffplay). |
Memory allocator callbacks:
Correct. It is the usually clean method of supporting Unreal engine's custom memory allocators. UE supports hot module reload (live coding) and other complex operations. It is preferable to have the cleanest interface possible. log and cpu trace callbacks: The approach we are proposing for TMV is to have a more complex "request-based" decoding API. We can elaborate that in parallel in the decoding api discussion, but for the purpose of this answer, the idea is that the complexity of the AU parsing and tile decoding worker thread management of TMV implementation is pushed in oapv itself, to facilitate implementation of a TMV-like player outside of Unreal. Because the decoding api is now request-based and more complex, profiling and error management is also more complex. In UE, the general approach in that case is to expose log and cpu trace to ease trouble shooting for errors or performance issues. We often ask users to run the problematic levels of their game with verbose logging and using Unreal Insights to capture traces, then send us the logs and traces so we can figure out what is wrong. So, those callbacks are not critical to this implementation (and we can debate the pertinence of each log individually), but have been proven to provide great help for long term user support and maintenance. |
@jsperrier1 — this really helps clarify the intent. Let me split my response into the two callback groups, since I think they need to be handled quite differently. Memory callbacksI fully understand the motivation (clean integration with Unreal's custom allocators, live coding, etc.), and I agree it's a reasonable thing to want. My main concern is security. Since liboapv parses untrusted bitstreams, allowing the allocator to be replaced can I don't want to just say no, though — I'd like to think more about whether there's a safe way to achieve the same Logging and CPU-trace callbacksLooking at the code, all of the log_msg() calls and the CPU-trace hooks live inside the new selective / Because of that, I'd prefer to review them together with the tile-based partial decoding discussion rather than |
@jsperrier1 , BTW, does it require to use the custom memory allocator for oapvm_xxx functions (metadata related functions)? The memory allocator could be introduced for encoder and decoder, but metadata manager seems to be a little difficult because |
Again, Hi @jsperrier1, The memory allocator will not use a process-global callback approach. A global, mutable set of function pointers Instead, a per-instance allocator has been implemented and pushed here: The allocator interface is supplied by the caller through the codec descriptor (oapve_cdesc_t / oapvd_cdesc_t) This has not been extended to the metadata API (oapvm_*). oapvm_create() has no descriptor to carry an allocator, Anyway, let me know your opinons. |
fine by me.
I am little on the fence for that one. Metadata allocations are byte-stream driven, so are attacker-influenced and not necessarily small. For my opinion, given that having all allocations instrumented is used in integration validation and troubleshooting. I would prefer not to leave stones unturned and have all byte-stream driven allocations instrumented. We have had memory corruptions to debug during integration (happens regularly). This is why I added the global hook to make sure I catch everything. Knowing meta data allocations are not instrumented, it becomes a special case that needs to be tracked and managed. For instance, I would have to make sure it is somehow disabled to make sure corruption doesn't come from that. Ideally, we should make sure that a NILL mid means to skip meta data parsing rather than failing the whole decode. For oapvd_decode_selective_multi_mips, we are currently not parsing the meta data so it is fine for TMV (for now, although, it probably should). But we also have the Electra/Protron player that has a vanilla OpenApv decoder which would parse meta data. My vote would be to extend memory hooks to metadata api to avoid making it a special case. |


Title: Add selective multi-mip tiled decoding, 16K tile support, per-frame RC, and codec callbacks
Summary
This PR adds a set of decoder/encoder capabilities developed for a tiled, multi-resolution ("mip") streaming workflow, ported onto current
main. The headline addition is a selective multi-mip tile decoder that decodes an arbitrary subset of tiles across multiple mip levels of an access unit in a single call, with coalesced I/O and multi-threaded tile decode. Supporting changes extend tile limits to 16K, add per-frame rate control, and introduce pluggable memory/logging/CPU-trace callbacks.Library (
src/,inc/) and app (app/) changes only. The test harness and media for this feature are substantial and are being finished on a separate branch; I'd like to land the library first and follow up with tests. Happy to reorder if you'd prefer them together.Motivation
For large images (up to 16K) encoded as tiled mip pyramids, clients often need only a viewport-dependent subset of tiles at a chosen resolution rather than a full-frame decode. The existing API decodes whole access units. This adds a random-access path that locates the requested mip frames, reads only the needed tile payloads (coalescing adjacent reads), and decodes the selected tiles in parallel.
What's included
Selective multi-mip decode (core)
oapvd_decode_selective_multi_mips()— decode a set of{mip level, tile coords}requests in one traversal, via a caller-supplied random-accessoapvd_istream_t(seek/read/tell). Per-request status is reported per mip; coalesced block I/O minimizes seeks; tiles decode across worker threads.16K tile support
OAPV_MAX_TILE_ROWS/OAPV_MAX_TILE_COLS), supporting 16K at 256x256 tiles. Tile arrays are allocated dynamically rather than fixed.Per-frame rate control
rc_param_frm[]), loaded per frame during encode.Bitstream:
tile_size_present_in_fh_flagoapve_param_t), defaulting to 0 (unchanged/base bitstream). When enabled it writes per-tile sizes into the frame header so a decoder can index individual tiles — required for the selective path. The encoder app sets it under--tmv-mips.Codec callbacks
oapv_set_logging_callback()/oapv_set_logging_verbosity()(OAPV_LOG_ERROR|WARNING|INFO|DEBUG), default WARNING.oapv_set_memory_callbacks()— pluggable allocators (NULL resets to libc).oapv_set_cputrace_callbacks()— optional CPU event tracing (NULL resets to no-op).inc/oapv.h.Encoder app (
app/)--tmv-mips— enables the tiled/mip workflow: setstile_size_present_in_fh_flagand emits per-AU tiled output..oapv/.apvoutput-extension handling.Public API additions (
inc/oapv.h)oapvd_decode_selective_multi_mips,oapvd_istream_toapv_mip_request_t,oapv_multi_mip_decode_toapv_set_logging_callback,oapv_set_logging_verbosity,oapv_log_callback_t,OAPV_LOG_*oapv_set_memory_callbacks,oapv_memory_callbacks_toapv_set_cputrace_callbacks,oapv_cputrace_callbacks_tOAPV_MAX_TILE_ROWS,OAPV_MAX_TILE_COLSBackward compatibility
tile_size_present_in_fh_flagdefaults to 0, callbacks default to prior behavior, and the standardoapvd_decode()path is untouched (the selective decoder uses its own frame-prepare that skips imgb validation because it decodes into caller buffers).ctestcases pass unchanged.Robustness / hardening
Given the recent fuzz-hardening work upstream (#224, #226), the selective path validates its inputs in the same spirit:
istream->read()return is checked; a short read flags the affected coalesced block, its tiles are skipped (never decoded from stale memory), and the call returnsOAPV_ERR_MALFORMED_BITSTREAM.tile_size_present_in_fh_flagis a hardOAPV_ERR_UNSUPPORTEDfor the selective path rather than a silent fallback.vsnprintf); NULL-safe callback setters.Testing
The dedicated test suite (a data-driven
decoder_test, multi-mip performance/error-handling programs, and media) lives on a separate branch and is not part of this PR. Validation performed there against this code: existingctestsuite 16/16; a 4K selective-decode case matrix (single/sparse/contiguous/strip tile sets across mip levels); multi-mip error-handling and performance programs; and a truncated/corrupt-input check that returns cleanly (no crash/hang) at 1/4/32 threads. I can bring a minimal subset of tests into this PR if you'd prefer coverage to land together.Notes for reviewers
oapvd_istream_t), distinct from the existing in-memory AU API.