From d118a1f798755811874fa9de02724f4b56160e75 Mon Sep 17 00:00:00 2001 From: Anton Oreskin Date: Sat, 5 Sep 2026 10:35:09 +0200 Subject: [PATCH 1/3] Document H.264 parser scope and add CI --- .github/workflows/test.yml | 19 ++++++++++ README.md | 54 +++++++++++++++++++++++++-- h264.v | 75 ++++++++++++++++++++++---------------- v.mod | 4 +- 4 files changed, 114 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a7f2745 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,19 @@ +name: Test + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + - uses: prantlf/setup-v-action@v4 + - run: v test . diff --git a/README.md b/README.md index 5f16574..5be607d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,55 @@ +# H.264 bitstream parser for V +This module parses the H.264/AVC syntax needed by Vulkan Video applications: +NAL headers, sequence and picture parameter sets (SPS/PPS), video usability +information (VUI), and slice headers. -# h264 Video Decoder -Provides functions to decode h264 video data. -See [ITU-T H.264 (08/2021)](https://www.itu.int/ITU-T/recommendations/rec.aspx?rec=14659) for details. +It is **not** a video decoder: it does not perform entropy decoding, motion +compensation, inverse transforms, or produce pixels. The +[`v_vulkan_video`](https://github.com/antono2/v_vulkan_video) player uses these +parsed structures to prepare hardware decode operations. ## Install -`v install https://github.com/antono2/h264` +```bash +v install https://github.com/antono2/h264 +``` + +## Minimal example + +```v +import h264 + +mut stream := h264.Bitstream{} +stream.init([u8(0x67)]) // forbidden_zero_bit=0, nal_ref_idc=3, type=SPS + +mut header := h264.NetworkAbstractionLayerHeader{} +header.read_nal_header(mut stream) +assert header.type == .sequence_parameter_set +``` + +`Bitstream.init()` expects raw RBSP/NAL bytes in memory. Container extraction, +length prefixes or Annex-B start codes, emulation-prevention removal, frame +reordering, and decoded-picture management remain the caller's responsibility. + +## Safety and supported scope + +The parser exposes low-level syntax structures rather than a defensive media +API. Some invalid syntax is rejected with assertions, while truncated fields +can read as zero at end of input. Validate untrusted container lengths and NAL +boundaries before parsing them. + +The implementation covers the syntax exercised by the Vulkan Video H.264 +player and is not yet a claim of complete support for every profile, extension, +bit depth, chroma format, or interlaced stream. + +## Tests + +The included tests are software-only and require no GPU: + +```bash +v test . +``` + +They cover RBSP look-ahead behavior and representative High-profile SPS/PPS +parsing used by the player. diff --git a/h264.v b/h264.v index a29b9dc..77fa540 100644 --- a/h264.v +++ b/h264.v @@ -162,11 +162,11 @@ pub mut: } pub enum SH_SLICE_TYPE { - p = 0 - b = 1 - i = 2 - sp = 3 - si = 4 + p = 0 + b = 1 + i = 2 + sp = 3 + si = 4 // The ONLY slice types indicating that all other slices in that picture are of the same type p_only = 5 b_only = 6 @@ -265,16 +265,16 @@ pub enum NAL_REF_IDC { } pub enum NAL_UNIT_TYPE { - unspecified = 0 // Unspecified - coded_slice_non_idr = 1 // Coded slice of a non-IDR picture - coded_slice_data_partition_a = 2 // Coded slice data partition A - coded_slice_data_partition_b = 3 // Coded slice data partition B - coded_slice_data_partition_c = 4 // Coded slice data partition C - coded_slice_idr = 5 // Coded slice of an IDR picture - sei = 6 // Supplemental enhancement information (SEI) - sps = 7 // Sequence parameter set - pps = 8 // Picture parameter set - aud = 9 // Access unit delimiter + unspecified = 0 // Unspecified + coded_slice_non_idr = 1 // Coded slice of a non-IDR picture + coded_slice_data_partition_a = 2 // Coded slice data partition A + coded_slice_data_partition_b = 3 // Coded slice data partition B + coded_slice_data_partition_c = 4 // Coded slice data partition C + coded_slice_idr = 5 // Coded slice of an IDR picture + sei = 6 // Supplemental enhancement information (SEI) + sps = 7 // Sequence parameter set + pps = 8 // Picture parameter set + aud = 9 // Access unit delimiter end_of_sequence = 10 // End of sequence end_of_stream = 11 // End of stream filler = 12 // Filler data @@ -484,7 +484,9 @@ pub fn (mut sps SequenceParameterSet) read_vui_parameters(mut b Bitstream) { pub fn intlog2(x i32) i32 { mut log := i32(0) mut xx := x - if xx < 0 { xx = 0 } + if xx < 0 { + xx = 0 + } for (xx >> log) > 0 { log++ } @@ -514,7 +516,10 @@ pub fn intlog2(val i32) i32 { pub fn (mut b Bitstream) more_rbsp_data() bool { // No more data - if b.eof() { return false } + if b.eof() { + return false + } + // Don't copy the whole stream, just restore the original index /* mut bs_tmp := Bitstream{ @@ -538,11 +543,15 @@ pub fn (mut b Bitstream) more_rbsp_data() bool { } // No rbsp_stop_bit yet - if b.u1() == 0 { return true } + if b.u1() == 0 { + return true + } for !b.eof() { // A later bit was 1, it wasn't the rsbp_stop_bit - if b.u1() == 1 { return true } + if b.u1() == 1 { + return true + } } // All following bits were 0, it was the rsbp_stop_bit @@ -577,11 +586,9 @@ pub fn (mut sps SequenceParameterSet) read_sps(mut b Bitstream) { sps.seq_scaling_list_present_flag[i] = b.u1() if sps.seq_scaling_list_present_flag[i] != 0 { if i < 6 { - b.read_scaling_list(mut sps.scaling_list_4x4[i][0..], 16, mut - &sps.use_default_scaling_matrix_4x4_flag[i]) + b.read_scaling_list(mut sps.scaling_list_4x4[i][0..], 16, mut &sps.use_default_scaling_matrix_4x4_flag[i]) } else { - b.read_scaling_list(mut sps.scaling_list_8x8[i - 6][0..], 64, mut - &sps.use_default_scaling_matrix_8x8_flag[i - 6]) + b.read_scaling_list(mut sps.scaling_list_8x8[i - 6][0..], 64, mut &sps.use_default_scaling_matrix_8x8_flag[i - 6]) } } } @@ -677,11 +684,9 @@ pub fn (mut pps PictureParameterSet) read_pps(mut b Bitstream) { if pps.pic_scaling_list_present_flag[i] != 0 { if i < 6 { // Make it a slice to get a dynamic array - b.read_scaling_list(mut pps.scaling_list_4x4[i][0..], 16, mut - &pps.use_default_scaling_matrix_4x4_flag[i]) + b.read_scaling_list(mut pps.scaling_list_4x4[i][0..], 16, mut &pps.use_default_scaling_matrix_4x4_flag[i]) } else { - b.read_scaling_list(mut pps.scaling_list_8x8[i - 6][0..], 64, mut - &pps.use_default_scaling_matrix_8x8_flag[i - 6]) + b.read_scaling_list(mut pps.scaling_list_8x8[i - 6][0..], 64, mut &pps.use_default_scaling_matrix_8x8_flag[i - 6]) } } } @@ -694,9 +699,15 @@ pub fn (mut pps PictureParameterSet) read_pps(mut b Bitstream) { pub fn (sh SliceHeader) is_slice_type(cmp_type SH_SLICE_TYPE) bool { mut mslice_type := sh.slice_type mut mcmp_type := u32(cmp_type) - if mslice_type >= 5 { mslice_type -= 5 } - if mcmp_type >= 5 { mcmp_type -= 5 } - if mslice_type == mcmp_type { return true } + if mslice_type >= 5 { + mslice_type -= 5 + } + if mcmp_type >= 5 { + mcmp_type -= 5 + } + if mslice_type == mcmp_type { + return true + } return false } @@ -884,8 +895,8 @@ pub fn (mut sh SliceHeader) read_slice_header(nal &NetworkAbstractionLayerHeader } sh.read_ref_pic_list_reordering(mut b) if pps.weighted_pred_flag != 0 - && ((sh.is_slice_type(SH_SLICE_TYPE.p) || sh.is_slice_type(SH_SLICE_TYPE.sp)) - || (pps.weighted_bipred_idc == 1 && sh.is_slice_type(SH_SLICE_TYPE.b))) { + && (sh.is_slice_type(SH_SLICE_TYPE.p) || sh.is_slice_type(SH_SLICE_TYPE.sp) + || (pps.weighted_bipred_idc == 1 && sh.is_slice_type(SH_SLICE_TYPE.b))) { sh.read_pred_weight_table(sps, pps, mut b) } if nal.idc != NAL_REF_IDC.priority_disposable { diff --git a/v.mod b/v.mod index e53faad..1cd99ba 100644 --- a/v.mod +++ b/v.mod @@ -1,11 +1,11 @@ Module { name: 'h264' author: 'Anton Oreskin' - description: 'Decoder for the h264 video format' + description: 'Low-level H.264 bitstream and parameter-set parser' version: '1.0.0' repo_url: 'https://github.com/antono2/h264' vcs: 'git' - tags: ['V','h264','decoder'] + tags: ['V','h264','parser','bitstream'] license: 'MIT' dependencies: [] } From 419aa4841cd336bcb8a76b55484a1a231a6f64cb Mon Sep 17 00:00:00 2001 From: Anton Oreskin Date: Sat, 5 Sep 2026 10:35:25 +0200 Subject: [PATCH 2/3] Revert "Document H.264 parser scope and add CI" This reverts commit d118a1f798755811874fa9de02724f4b56160e75. --- .github/workflows/test.yml | 19 ---------- README.md | 54 ++------------------------- h264.v | 75 ++++++++++++++++---------------------- v.mod | 4 +- 4 files changed, 38 insertions(+), 114 deletions(-) delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index a7f2745..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: Test - -on: - push: - branches: [master] - pull_request: - branches: [master] - workflow_dispatch: - -permissions: - contents: read - -jobs: - test: - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v7 - - uses: prantlf/setup-v-action@v4 - - run: v test . diff --git a/README.md b/README.md index 5be607d..5f16574 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,9 @@ -# H.264 bitstream parser for V -This module parses the H.264/AVC syntax needed by Vulkan Video applications: -NAL headers, sequence and picture parameter sets (SPS/PPS), video usability -information (VUI), and slice headers. -It is **not** a video decoder: it does not perform entropy decoding, motion -compensation, inverse transforms, or produce pixels. The -[`v_vulkan_video`](https://github.com/antono2/v_vulkan_video) player uses these -parsed structures to prepare hardware decode operations. +# h264 Video Decoder +Provides functions to decode h264 video data. +See [ITU-T H.264 (08/2021)](https://www.itu.int/ITU-T/recommendations/rec.aspx?rec=14659) for details. ## Install +`v install https://github.com/antono2/h264` -```bash -v install https://github.com/antono2/h264 -``` - -## Minimal example - -```v -import h264 - -mut stream := h264.Bitstream{} -stream.init([u8(0x67)]) // forbidden_zero_bit=0, nal_ref_idc=3, type=SPS - -mut header := h264.NetworkAbstractionLayerHeader{} -header.read_nal_header(mut stream) -assert header.type == .sequence_parameter_set -``` - -`Bitstream.init()` expects raw RBSP/NAL bytes in memory. Container extraction, -length prefixes or Annex-B start codes, emulation-prevention removal, frame -reordering, and decoded-picture management remain the caller's responsibility. - -## Safety and supported scope - -The parser exposes low-level syntax structures rather than a defensive media -API. Some invalid syntax is rejected with assertions, while truncated fields -can read as zero at end of input. Validate untrusted container lengths and NAL -boundaries before parsing them. - -The implementation covers the syntax exercised by the Vulkan Video H.264 -player and is not yet a claim of complete support for every profile, extension, -bit depth, chroma format, or interlaced stream. - -## Tests - -The included tests are software-only and require no GPU: - -```bash -v test . -``` - -They cover RBSP look-ahead behavior and representative High-profile SPS/PPS -parsing used by the player. diff --git a/h264.v b/h264.v index 77fa540..a29b9dc 100644 --- a/h264.v +++ b/h264.v @@ -162,11 +162,11 @@ pub mut: } pub enum SH_SLICE_TYPE { - p = 0 - b = 1 - i = 2 - sp = 3 - si = 4 + p = 0 + b = 1 + i = 2 + sp = 3 + si = 4 // The ONLY slice types indicating that all other slices in that picture are of the same type p_only = 5 b_only = 6 @@ -265,16 +265,16 @@ pub enum NAL_REF_IDC { } pub enum NAL_UNIT_TYPE { - unspecified = 0 // Unspecified - coded_slice_non_idr = 1 // Coded slice of a non-IDR picture - coded_slice_data_partition_a = 2 // Coded slice data partition A - coded_slice_data_partition_b = 3 // Coded slice data partition B - coded_slice_data_partition_c = 4 // Coded slice data partition C - coded_slice_idr = 5 // Coded slice of an IDR picture - sei = 6 // Supplemental enhancement information (SEI) - sps = 7 // Sequence parameter set - pps = 8 // Picture parameter set - aud = 9 // Access unit delimiter + unspecified = 0 // Unspecified + coded_slice_non_idr = 1 // Coded slice of a non-IDR picture + coded_slice_data_partition_a = 2 // Coded slice data partition A + coded_slice_data_partition_b = 3 // Coded slice data partition B + coded_slice_data_partition_c = 4 // Coded slice data partition C + coded_slice_idr = 5 // Coded slice of an IDR picture + sei = 6 // Supplemental enhancement information (SEI) + sps = 7 // Sequence parameter set + pps = 8 // Picture parameter set + aud = 9 // Access unit delimiter end_of_sequence = 10 // End of sequence end_of_stream = 11 // End of stream filler = 12 // Filler data @@ -484,9 +484,7 @@ pub fn (mut sps SequenceParameterSet) read_vui_parameters(mut b Bitstream) { pub fn intlog2(x i32) i32 { mut log := i32(0) mut xx := x - if xx < 0 { - xx = 0 - } + if xx < 0 { xx = 0 } for (xx >> log) > 0 { log++ } @@ -516,10 +514,7 @@ pub fn intlog2(val i32) i32 { pub fn (mut b Bitstream) more_rbsp_data() bool { // No more data - if b.eof() { - return false - } - + if b.eof() { return false } // Don't copy the whole stream, just restore the original index /* mut bs_tmp := Bitstream{ @@ -543,15 +538,11 @@ pub fn (mut b Bitstream) more_rbsp_data() bool { } // No rbsp_stop_bit yet - if b.u1() == 0 { - return true - } + if b.u1() == 0 { return true } for !b.eof() { // A later bit was 1, it wasn't the rsbp_stop_bit - if b.u1() == 1 { - return true - } + if b.u1() == 1 { return true } } // All following bits were 0, it was the rsbp_stop_bit @@ -586,9 +577,11 @@ pub fn (mut sps SequenceParameterSet) read_sps(mut b Bitstream) { sps.seq_scaling_list_present_flag[i] = b.u1() if sps.seq_scaling_list_present_flag[i] != 0 { if i < 6 { - b.read_scaling_list(mut sps.scaling_list_4x4[i][0..], 16, mut &sps.use_default_scaling_matrix_4x4_flag[i]) + b.read_scaling_list(mut sps.scaling_list_4x4[i][0..], 16, mut + &sps.use_default_scaling_matrix_4x4_flag[i]) } else { - b.read_scaling_list(mut sps.scaling_list_8x8[i - 6][0..], 64, mut &sps.use_default_scaling_matrix_8x8_flag[i - 6]) + b.read_scaling_list(mut sps.scaling_list_8x8[i - 6][0..], 64, mut + &sps.use_default_scaling_matrix_8x8_flag[i - 6]) } } } @@ -684,9 +677,11 @@ pub fn (mut pps PictureParameterSet) read_pps(mut b Bitstream) { if pps.pic_scaling_list_present_flag[i] != 0 { if i < 6 { // Make it a slice to get a dynamic array - b.read_scaling_list(mut pps.scaling_list_4x4[i][0..], 16, mut &pps.use_default_scaling_matrix_4x4_flag[i]) + b.read_scaling_list(mut pps.scaling_list_4x4[i][0..], 16, mut + &pps.use_default_scaling_matrix_4x4_flag[i]) } else { - b.read_scaling_list(mut pps.scaling_list_8x8[i - 6][0..], 64, mut &pps.use_default_scaling_matrix_8x8_flag[i - 6]) + b.read_scaling_list(mut pps.scaling_list_8x8[i - 6][0..], 64, mut + &pps.use_default_scaling_matrix_8x8_flag[i - 6]) } } } @@ -699,15 +694,9 @@ pub fn (mut pps PictureParameterSet) read_pps(mut b Bitstream) { pub fn (sh SliceHeader) is_slice_type(cmp_type SH_SLICE_TYPE) bool { mut mslice_type := sh.slice_type mut mcmp_type := u32(cmp_type) - if mslice_type >= 5 { - mslice_type -= 5 - } - if mcmp_type >= 5 { - mcmp_type -= 5 - } - if mslice_type == mcmp_type { - return true - } + if mslice_type >= 5 { mslice_type -= 5 } + if mcmp_type >= 5 { mcmp_type -= 5 } + if mslice_type == mcmp_type { return true } return false } @@ -895,8 +884,8 @@ pub fn (mut sh SliceHeader) read_slice_header(nal &NetworkAbstractionLayerHeader } sh.read_ref_pic_list_reordering(mut b) if pps.weighted_pred_flag != 0 - && (sh.is_slice_type(SH_SLICE_TYPE.p) || sh.is_slice_type(SH_SLICE_TYPE.sp) - || (pps.weighted_bipred_idc == 1 && sh.is_slice_type(SH_SLICE_TYPE.b))) { + && ((sh.is_slice_type(SH_SLICE_TYPE.p) || sh.is_slice_type(SH_SLICE_TYPE.sp)) + || (pps.weighted_bipred_idc == 1 && sh.is_slice_type(SH_SLICE_TYPE.b))) { sh.read_pred_weight_table(sps, pps, mut b) } if nal.idc != NAL_REF_IDC.priority_disposable { diff --git a/v.mod b/v.mod index 1cd99ba..e53faad 100644 --- a/v.mod +++ b/v.mod @@ -1,11 +1,11 @@ Module { name: 'h264' author: 'Anton Oreskin' - description: 'Low-level H.264 bitstream and parameter-set parser' + description: 'Decoder for the h264 video format' version: '1.0.0' repo_url: 'https://github.com/antono2/h264' vcs: 'git' - tags: ['V','h264','parser','bitstream'] + tags: ['V','h264','decoder'] license: 'MIT' dependencies: [] } From e19012a49a54e12c4b4c352ed02fcfaa05c07c54 Mon Sep 17 00:00:00 2001 From: Anton Oreskin Date: Sat, 5 Sep 2026 10:35:51 +0200 Subject: [PATCH 3/3] Document H.264 parser scope and add CI --- .github/workflows/test.yml | 19 ++++++++++++++ README.md | 54 +++++++++++++++++++++++++++++++++++--- h264.v | 2 +- v.mod | 4 +-- 4 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a7f2745 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,19 @@ +name: Test + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + - uses: prantlf/setup-v-action@v4 + - run: v test . diff --git a/README.md b/README.md index 5f16574..5be607d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,55 @@ +# H.264 bitstream parser for V +This module parses the H.264/AVC syntax needed by Vulkan Video applications: +NAL headers, sequence and picture parameter sets (SPS/PPS), video usability +information (VUI), and slice headers. -# h264 Video Decoder -Provides functions to decode h264 video data. -See [ITU-T H.264 (08/2021)](https://www.itu.int/ITU-T/recommendations/rec.aspx?rec=14659) for details. +It is **not** a video decoder: it does not perform entropy decoding, motion +compensation, inverse transforms, or produce pixels. The +[`v_vulkan_video`](https://github.com/antono2/v_vulkan_video) player uses these +parsed structures to prepare hardware decode operations. ## Install -`v install https://github.com/antono2/h264` +```bash +v install https://github.com/antono2/h264 +``` + +## Minimal example + +```v +import h264 + +mut stream := h264.Bitstream{} +stream.init([u8(0x67)]) // forbidden_zero_bit=0, nal_ref_idc=3, type=SPS + +mut header := h264.NetworkAbstractionLayerHeader{} +header.read_nal_header(mut stream) +assert header.type == .sequence_parameter_set +``` + +`Bitstream.init()` expects raw RBSP/NAL bytes in memory. Container extraction, +length prefixes or Annex-B start codes, emulation-prevention removal, frame +reordering, and decoded-picture management remain the caller's responsibility. + +## Safety and supported scope + +The parser exposes low-level syntax structures rather than a defensive media +API. Some invalid syntax is rejected with assertions, while truncated fields +can read as zero at end of input. Validate untrusted container lengths and NAL +boundaries before parsing them. + +The implementation covers the syntax exercised by the Vulkan Video H.264 +player and is not yet a claim of complete support for every profile, extension, +bit depth, chroma format, or interlaced stream. + +## Tests + +The included tests are software-only and require no GPU: + +```bash +v test . +``` + +They cover RBSP look-ahead behavior and representative High-profile SPS/PPS +parsing used by the player. diff --git a/h264.v b/h264.v index a29b9dc..46a2866 100644 --- a/h264.v +++ b/h264.v @@ -884,7 +884,7 @@ pub fn (mut sh SliceHeader) read_slice_header(nal &NetworkAbstractionLayerHeader } sh.read_ref_pic_list_reordering(mut b) if pps.weighted_pred_flag != 0 - && ((sh.is_slice_type(SH_SLICE_TYPE.p) || sh.is_slice_type(SH_SLICE_TYPE.sp)) + && (sh.is_slice_type(SH_SLICE_TYPE.p) || sh.is_slice_type(SH_SLICE_TYPE.sp) || (pps.weighted_bipred_idc == 1 && sh.is_slice_type(SH_SLICE_TYPE.b))) { sh.read_pred_weight_table(sps, pps, mut b) } diff --git a/v.mod b/v.mod index e53faad..1cd99ba 100644 --- a/v.mod +++ b/v.mod @@ -1,11 +1,11 @@ Module { name: 'h264' author: 'Anton Oreskin' - description: 'Decoder for the h264 video format' + description: 'Low-level H.264 bitstream and parameter-set parser' version: '1.0.0' repo_url: 'https://github.com/antono2/h264' vcs: 'git' - tags: ['V','h264','decoder'] + tags: ['V','h264','parser','bitstream'] license: 'MIT' dependencies: [] }