aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2025-03-05 17:38:04 +0000
committers-ol <s+removethis@s-ol.nu>2025-03-10 11:44:48 +0000
commitcf3475eab439df049fb48a039801da8270dc044c (patch)
tree006e6a677696f9ce6e030614167158ff6a84c682
parentUpdate README.md (diff)
downloadglsl-view-cf3475eab439df049fb48a039801da8270dc044c.tar.gz
glsl-view-cf3475eab439df049fb48a039801da8270dc044c.zip
implement HAP decoding straight to compressed GPU texture
Supports HAP and HAP Alpha (Hap1/Hap5) formats
-rw-r--r--README.md8
-rw-r--r--build.zig8
-rw-r--r--lib/hap.c1188
-rw-r--r--lib/hap.h158
-rw-r--r--src/c.zig4
-rw-r--r--src/control.zig3
-rw-r--r--src/gl.zig111
-rw-r--r--src/video.zig407
8 files changed, 1762 insertions, 125 deletions
diff --git a/README.md b/README.md
index 104f0d9..df243f1 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,14 @@ When built with ffmpeg, images and videos can be loaded as 2D or 3D textures:
The filepaths are interpreted relative to the config file.
+#### video files
+Videos are fully uploaded to the GPU as 2D-array- or 3D textures.
+To save on GPU memory, textures are stored using GPU compression.
+Nevertheless, the resolution and length are limited.
+
+Loading most video formats is quite slow because the frames need to be compressed during upload.
+Videos encoded using HAP or HAP Alpha will load much faster since the image data is already in the GPU compressed format.
+
### commands
A message to `/-/reload` forces a reload of the shader source code.
diff --git a/build.zig b/build.zig
index 9c842a6..449c82e 100644
--- a/build.zig
+++ b/build.zig
@@ -5,6 +5,7 @@ pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const have_ffmpeg = b.option(bool, "textures", "enable texture/video support (needs ffmpeg)") orelse true;
+ const have_hap = have_ffmpeg and b.option(bool, "hap", "enable HAP GPU upload support (need snappy)") orelse true;
const exe = b.addExecutable(.{
.name = "glsl-view",
@@ -15,6 +16,7 @@ pub fn build(b: *std.Build) void {
const options = b.addOptions();
options.addOption(bool, "have_ffmpeg", have_ffmpeg);
+ options.addOption(bool, "have_hap", have_hap);
exe.root_module.addOptions("build_config", options);
exe.linkLibC();
@@ -30,6 +32,12 @@ pub fn build(b: *std.Build) void {
exe.linkSystemLibrary("libswscale");
}
+ if (have_ffmpeg and have_hap) {
+ exe.addIncludePath(b.path("lib"));
+ exe.addCSourceFile(.{ .file = b.path("lib/hap.c") });
+ exe.linkSystemLibrary("snappy");
+ }
+
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
diff --git a/lib/hap.c b/lib/hap.c
new file mode 100644
index 0000000..6c492cc
--- /dev/null
+++ b/lib/hap.c
@@ -0,0 +1,1188 @@
+/*
+ hap.c
+
+ Copyright (c) 2011-2013, Tom Butterworth and Vidvox LLC. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "hap.h"
+#include <stdlib.h>
+#include <stdint.h>
+#include <string.h> // For memcpy for uncompressed frames
+#include "snappy-c.h"
+
+#define kHapUInt24Max 0x00FFFFFF
+
+/*
+ Hap Constants
+ First four bits represent the compressor
+ Second four bits represent the format
+ */
+#define kHapCompressorNone 0xA
+#define kHapCompressorSnappy 0xB
+#define kHapCompressorComplex 0xC
+
+#define kHapFormatRGBDXT1 0xB
+#define kHapFormatRGBADXT5 0xE
+#define kHapFormatYCoCgDXT5 0xF
+#define kHapFormatARGTC1 0x1
+#define kHapFormatRGBABPTC 0xC
+#define kHapFormatRGBBPTCUF 0x2
+#define kHapFormatRGBBPTCSF 0x3
+
+/*
+ Packed byte values for Hap
+
+ Format Compressor Byte Code
+ ----------------------------------------------------
+ RGB_DXT1 None 0xAB
+ RGB_DXT1 Snappy 0xBB
+ RGB_DXT1 Complex 0xCB
+ RGBA_DXT5 None 0xAE
+ RGBA_DXT5 Snappy 0xBE
+ RGBA_DXT5 Complex 0xCE
+ YCoCg_DXT5 None 0xAF
+ YCoCg_DXT5 Snappy 0xBF
+ YCoCg_DXT5 Complex 0xCF
+ A_RGTC1 None 0xA1
+ A_RGTC1 Snappy 0xB1
+ A_RGTC1 Complex 0xC1
+ RGBA_BPTC_UNORM None 0xAC
+ RGBA_BPTC_UNORM Snappy 0xBC
+ RGBA_BPTC_UNORM Complex 0xCC
+ RGB_BPTC_UNSIGNED_FLOAT None 0xA2
+ RGB_BPTC_UNSIGNED_FLOAT Snappy 0xB2
+ RGB_BPTC_UNSIGNED_FLOAT Complex 0xC2
+ RGB_BPTC_SIGNED_FLOAT None 0xA3
+ RGB_BPTC_SIGNED_FLOAT Snappy 0xB3
+ RGB_BPTC_SIGNED_FLOAT Complex 0xC3
+ */
+
+/*
+ Hap Frame Section Types
+ */
+#define kHapSectionMultipleImages 0x0D
+#define kHapSectionDecodeInstructionsContainer 0x01
+#define kHapSectionChunkSecondStageCompressorTable 0x02
+#define kHapSectionChunkSizeTable 0x03
+#define kHapSectionChunkOffsetTable 0x04
+
+/*
+ To decode we use a struct to store details of each chunk
+ */
+typedef struct HapChunkDecodeInfo {
+ unsigned int result;
+ unsigned int compressor;
+ const char *compressed_chunk_data;
+ size_t compressed_chunk_size;
+ char *uncompressed_chunk_data;
+ size_t uncompressed_chunk_size;
+} HapChunkDecodeInfo;
+
+// TODO: rename the defines we use for codes used in stored frames
+// to better differentiate them from the enums used for the API
+
+// These read and write little-endian values on big or little-endian architectures
+static unsigned int hap_read_3_byte_uint(const void *buffer)
+{
+ return (*(uint8_t *)buffer) + ((*(((uint8_t *)buffer) + 1)) << 8) + ((*(((uint8_t *)buffer) + 2)) << 16);
+}
+
+static void hap_write_3_byte_uint(void *buffer, unsigned int value)
+{
+ *(uint8_t *)buffer = value & 0xFF;
+ *(((uint8_t *)buffer) + 1) = (value >> 8) & 0xFF;
+ *(((uint8_t *)buffer) + 2) = (value >> 16) & 0xFF;
+}
+
+static unsigned int hap_read_4_byte_uint(const void *buffer)
+{
+ return (*(uint8_t *)buffer) + ((*(((uint8_t *)buffer) + 1)) << 8) + ((*(((uint8_t *)buffer) + 2)) << 16) + ((*(((uint8_t *)buffer) + 3)) << 24);
+}
+
+static void hap_write_4_byte_uint(const void *buffer, unsigned int value)
+{
+ *(uint8_t *)buffer = value & 0xFF;
+ *(((uint8_t *)buffer) + 1) = (value >> 8) & 0xFF;
+ *(((uint8_t *)buffer) + 2) = (value >> 16) & 0xFF;
+ *(((uint8_t *)buffer) + 3) = (value >> 24) & 0xFF;
+}
+
+#define hap_top_4_bits(x) (((x) & 0xF0) >> 4)
+
+#define hap_bottom_4_bits(x) ((x) & 0x0F)
+
+#define hap_4_bit_packed_byte(top_bits, bottom_bits) (((top_bits) << 4) | ((bottom_bits) & 0x0F))
+
+static int hap_read_section_header(const void *buffer, uint32_t buffer_length, uint32_t *out_header_length, uint32_t *out_section_length, unsigned int *out_section_type)
+{
+ /*
+ Verify buffer is big enough to contain a four-byte header
+ */
+ if (buffer_length < 4U)
+ {
+ return HapResult_Bad_Frame;
+ }
+
+ /*
+ The first three bytes are the length of the section (not including the header) or zero
+ if the length is stored in the last four bytes of an eight-byte header
+ */
+ *out_section_length = hap_read_3_byte_uint(buffer);
+
+ /*
+ If the first three bytes are zero, the size is in the following four bytes
+ */
+ if (*out_section_length == 0U)
+ {
+ /*
+ Verify buffer is big enough to contain an eight-byte header
+ */
+ if (buffer_length < 8U)
+ {
+ return HapResult_Bad_Frame;
+ }
+ *out_section_length = hap_read_4_byte_uint(((uint8_t *)buffer) + 4U);
+ *out_header_length = 8U;
+ }
+ else
+ {
+ *out_header_length = 4U;
+ }
+
+ /*
+ The fourth byte stores the section type
+ */
+ *out_section_type = *(((uint8_t *)buffer) + 3U);
+
+ /*
+ Verify the section does not extend beyond the buffer
+ */
+ if (*out_header_length + *out_section_length > buffer_length)
+ {
+ return HapResult_Bad_Frame;
+ }
+
+ return HapResult_No_Error;
+}
+
+static void hap_write_section_header(void *buffer, size_t header_length, uint32_t section_length, unsigned int section_type)
+{
+ /*
+ The first three bytes are the length of the section (not including the header) or zero
+ if using an eight-byte header
+ */
+ if (header_length == 4U)
+ {
+ hap_write_3_byte_uint(buffer, (unsigned int)section_length);
+ }
+ else
+ {
+ /*
+ For an eight-byte header, the length is in the last four bytes
+ */
+ hap_write_3_byte_uint(buffer, 0U);
+ hap_write_4_byte_uint(((uint8_t *)buffer) + 4U, section_length);
+ }
+
+ /*
+ The fourth byte stores the section type
+ */
+ *(((uint8_t *)buffer) + 3) = section_type;
+}
+
+// Returns an API texture format constant or 0 if not recognised
+static unsigned int hap_texture_format_constant_for_format_identifier(unsigned int identifier)
+{
+ switch (identifier)
+ {
+ case kHapFormatRGBDXT1:
+ return HapTextureFormat_RGB_DXT1;
+ case kHapFormatRGBADXT5:
+ return HapTextureFormat_RGBA_DXT5;
+ case kHapFormatYCoCgDXT5:
+ return HapTextureFormat_YCoCg_DXT5;
+ case kHapFormatARGTC1:
+ return HapTextureFormat_A_RGTC1;
+ case kHapFormatRGBABPTC:
+ return HapTextureFormat_RGBA_BPTC_UNORM;
+ case kHapFormatRGBBPTCUF:
+ return HapTextureFormat_RGB_BPTC_UNSIGNED_FLOAT;
+ case kHapFormatRGBBPTCSF:
+ return HapTextureFormat_RGB_BPTC_SIGNED_FLOAT;
+ default:
+ return 0;
+
+ }
+}
+
+// Returns a frame identifier or 0 if not recognised
+static unsigned int hap_texture_format_identifier_for_format_constant(unsigned int constant)
+{
+ switch (constant)
+ {
+ case HapTextureFormat_RGB_DXT1:
+ return kHapFormatRGBDXT1;
+ case HapTextureFormat_RGBA_DXT5:
+ return kHapFormatRGBADXT5;
+ case HapTextureFormat_YCoCg_DXT5:
+ return kHapFormatYCoCgDXT5;
+ case HapTextureFormat_A_RGTC1:
+ return kHapFormatARGTC1;
+ case HapTextureFormat_RGBA_BPTC_UNORM:
+ return kHapFormatRGBABPTC;
+ case HapTextureFormat_RGB_BPTC_UNSIGNED_FLOAT:
+ return kHapFormatRGBBPTCUF;
+ case HapTextureFormat_RGB_BPTC_SIGNED_FLOAT:
+ return kHapFormatRGBBPTCSF;
+ default:
+ return 0;
+ }
+}
+
+// Returns the length of a decode instructions container of chunk_count chunks
+// not including the section header
+static size_t hap_decode_instructions_length(unsigned int chunk_count)
+{
+ /*
+ Calculate the size of our Decode Instructions Section
+ = Second-Stage Compressor Table + Chunk Size Table + headers for both sections
+ = chunk_count + (4 * chunk_count) + 4 + 4
+ */
+ size_t length = (5 * chunk_count) + 8;
+
+ return length;
+}
+
+static unsigned int hap_limited_chunk_count_for_frame(size_t input_bytes, unsigned int texture_format, unsigned int chunk_count)
+{
+ // This is a hard limit due to the 4-byte headers we use for the decode instruction container
+ // (0xFFFFFF == count + (4 x count) + 20)
+ if (chunk_count > 3355431)
+ {
+ chunk_count = 3355431;
+ }
+ // Divide frame equally on DXT block boundries (8 or 16 bytes)
+ unsigned long dxt_block_count;
+ switch (texture_format) {
+ case HapTextureFormat_RGB_DXT1:
+ case HapTextureFormat_A_RGTC1:
+ dxt_block_count = input_bytes / 8;
+ break;
+ default:
+ dxt_block_count = input_bytes / 16;
+ }
+ while (dxt_block_count % chunk_count != 0) {
+ chunk_count--;
+ }
+
+ return chunk_count;
+}
+
+static size_t hap_max_encoded_length(size_t input_bytes, unsigned int texture_format, unsigned int compressor, unsigned int chunk_count)
+{
+ size_t decode_instructions_length, max_compressed_length;
+
+ chunk_count = hap_limited_chunk_count_for_frame(input_bytes, texture_format, chunk_count);
+
+ decode_instructions_length = hap_decode_instructions_length(chunk_count);
+
+ if (compressor == HapCompressorSnappy)
+ {
+ size_t chunk_size = input_bytes / chunk_count;
+ max_compressed_length = snappy_max_compressed_length(chunk_size) * chunk_count;
+ }
+ else
+ {
+ max_compressed_length = input_bytes;
+ }
+
+ // top section header + decode instructions section header + decode instructions + compressed data
+ return max_compressed_length + 8U + decode_instructions_length + 4U;
+}
+
+unsigned long HapMaxEncodedLength(unsigned int count,
+ unsigned long *inputBytes,
+ unsigned int *textureFormats,
+ unsigned int *chunkCounts)
+{
+ // Start with the length of a multiple-image section header
+ unsigned long total_length = 8;
+
+ // Return 0 for bad arguments
+ if (count == 0 || count > 2
+ || inputBytes == NULL
+ || textureFormats == NULL
+ || chunkCounts == NULL)
+ {
+ return 0;
+ }
+
+ for (int i = 0; i < count; i++)
+ {
+ if (chunkCounts[i] == 0)
+ {
+ return 0;
+ }
+
+ // Assume snappy, the worst case
+ total_length += hap_max_encoded_length(inputBytes[i], textureFormats[i], HapCompressorSnappy, chunkCounts[i]);
+ }
+
+ return total_length;
+}
+
+static unsigned int hap_encode_texture(const void *inputBuffer, unsigned long inputBufferBytes, unsigned int textureFormat,
+ unsigned int compressor, unsigned int chunkCount, void *outputBuffer,
+ unsigned long outputBufferBytes, unsigned long *outputBufferBytesUsed)
+{
+ size_t top_section_header_length;
+ size_t top_section_length;
+ unsigned int storedCompressor;
+ unsigned int storedFormat;
+
+ /*
+ Check arguments
+ */
+ if (inputBuffer == NULL
+ || inputBufferBytes == 0
+ || (textureFormat != HapTextureFormat_RGB_DXT1
+ && textureFormat != HapTextureFormat_RGBA_DXT5
+ && textureFormat != HapTextureFormat_YCoCg_DXT5
+ && textureFormat != HapTextureFormat_A_RGTC1
+ && textureFormat != HapTextureFormat_RGBA_BPTC_UNORM
+ && textureFormat != HapTextureFormat_RGB_BPTC_UNSIGNED_FLOAT
+ && textureFormat != HapTextureFormat_RGB_BPTC_SIGNED_FLOAT
+ )
+ || (compressor != HapCompressorNone
+ && compressor != HapCompressorSnappy
+ )
+ || outputBuffer == NULL
+ || outputBufferBytesUsed == NULL
+ )
+ {
+ return HapResult_Bad_Arguments;
+ }
+ else if (outputBufferBytes < hap_max_encoded_length(inputBufferBytes, textureFormat, compressor, chunkCount))
+ {
+ return HapResult_Buffer_Too_Small;
+ }
+
+ /*
+ To store frames of length greater than can be expressed in three bytes, we use an eight byte header (the last four bytes are the
+ frame size). We don't know the compressed size until we have performed compression, but we know the worst-case size
+ (the uncompressed size), so choose header-length based on that.
+
+ A simpler encoder could always use the eight-byte header variation.
+ */
+ if (inputBufferBytes > kHapUInt24Max)
+ {
+ top_section_header_length = 8U;
+ }
+ else
+ {
+ top_section_header_length = 4U;
+ }
+
+ if (compressor == HapCompressorSnappy)
+ {
+ /*
+ We attempt to chunk as requested, and if resulting frame is larger than it is uncompressed then
+ store frame uncompressed
+ */
+
+ size_t decode_instructions_length;
+ size_t chunk_size, compress_buffer_remaining;
+ uint8_t *second_stage_compressor_table;
+ void *chunk_size_table;
+ char *compressed_data;
+ unsigned int i;
+
+ chunkCount = hap_limited_chunk_count_for_frame(inputBufferBytes, textureFormat, chunkCount);
+ decode_instructions_length = hap_decode_instructions_length(chunkCount);
+
+ // Check we have space for the Decode Instructions Container
+ if ((inputBufferBytes + decode_instructions_length + 4) > kHapUInt24Max)
+ {
+ top_section_header_length = 8U;
+ }
+
+ second_stage_compressor_table = ((uint8_t *)outputBuffer) + top_section_header_length + 4 + 4;
+ chunk_size_table = ((uint8_t *)outputBuffer) + top_section_header_length + 4 + 4 + chunkCount + 4;
+
+ chunk_size = inputBufferBytes / chunkCount;
+
+ // write the Decode Instructions section header
+ hap_write_section_header(((uint8_t *)outputBuffer) + top_section_header_length, 4U, decode_instructions_length, kHapSectionDecodeInstructionsContainer);
+ // write the Second Stage Compressor Table section header
+ hap_write_section_header(((uint8_t *)outputBuffer) + top_section_header_length + 4U, 4U, chunkCount, kHapSectionChunkSecondStageCompressorTable);
+ // write the Chunk Size Table section header
+ hap_write_section_header(((uint8_t *)outputBuffer) + top_section_header_length + 4U + 4U + chunkCount, 4U, chunkCount * 4U, kHapSectionChunkSizeTable);
+
+ compressed_data = (char *)(((uint8_t *)outputBuffer) + top_section_header_length + 4 + decode_instructions_length);
+
+ compress_buffer_remaining = outputBufferBytes - top_section_header_length - 4 - decode_instructions_length;
+
+ top_section_length = 4 + decode_instructions_length;
+
+ for (i = 0; i < chunkCount; i++) {
+ size_t chunk_packed_length = compress_buffer_remaining;
+ const char *chunk_input_start = (const char *)(((uint8_t *)inputBuffer) + (chunk_size * i));
+ if (compressor == HapCompressorSnappy)
+ {
+ snappy_status result = snappy_compress(chunk_input_start, chunk_size, (char *)compressed_data, &chunk_packed_length);
+ if (result != SNAPPY_OK)
+ {
+ return HapResult_Internal_Error;
+ }
+ }
+
+ if (compressor == HapCompressorNone || chunk_packed_length >= chunk_size)
+ {
+ // store the chunk uncompressed
+ memcpy(compressed_data, chunk_input_start, chunk_size);
+ chunk_packed_length = chunk_size;
+ second_stage_compressor_table[i] = kHapCompressorNone;
+ }
+ else
+ {
+ // ie we used snappy and saved some space
+ second_stage_compressor_table[i] = kHapCompressorSnappy;
+ }
+ hap_write_4_byte_uint(((uint8_t *)chunk_size_table) + (i * 4), chunk_packed_length);
+ compressed_data += chunk_packed_length;
+ top_section_length += chunk_packed_length;
+ compress_buffer_remaining -= chunk_packed_length;
+ }
+
+ if (top_section_length < inputBufferBytes + top_section_header_length)
+ {
+ // use the complex storage because snappy compression saved space
+ storedCompressor = kHapCompressorComplex;
+ }
+ else
+ {
+ // Signal to store the frame uncompressed
+ compressor = HapCompressorNone;
+ }
+ }
+
+ if (compressor == HapCompressorNone)
+ {
+ memcpy(((uint8_t *)outputBuffer) + top_section_header_length, inputBuffer, inputBufferBytes);
+ top_section_length = inputBufferBytes;
+ storedCompressor = kHapCompressorNone;
+ }
+
+ storedFormat = hap_texture_format_identifier_for_format_constant(textureFormat);
+
+ hap_write_section_header(outputBuffer, top_section_header_length, top_section_length, hap_4_bit_packed_byte(storedCompressor, storedFormat));
+
+ *outputBufferBytesUsed = top_section_length + top_section_header_length;
+
+ return HapResult_No_Error;
+}
+
+unsigned int HapEncode(unsigned int count,
+ const void **inputBuffers, unsigned long *inputBuffersBytes,
+ unsigned int *textureFormats,
+ unsigned int *compressors,
+ unsigned int *chunkCounts,
+ void *outputBuffer, unsigned long outputBufferBytes,
+ unsigned long *outputBufferBytesUsed)
+{
+ size_t top_section_header_length;
+ size_t top_section_length;
+ unsigned long section_length;
+
+ if (count == 0 || count > 2 // A frame must contain one or two textures
+ || inputBuffers == NULL
+ || inputBuffersBytes == NULL
+ || textureFormats == NULL
+ || compressors == NULL
+ || chunkCounts == NULL
+ || outputBuffer == NULL
+ || outputBufferBytes == 0
+ || outputBufferBytesUsed == NULL)
+ {
+ return HapResult_Bad_Arguments;
+ }
+
+ for (int i = 0; i < count; i++)
+ {
+ if (chunkCounts[i] == 0)
+ {
+ return HapResult_Bad_Arguments;
+ }
+ }
+
+ if (count == 1)
+ {
+ // Encode without the multi-image layout
+ return hap_encode_texture(inputBuffers[0],
+ inputBuffersBytes[0],
+ textureFormats[0],
+ compressors[0],
+ chunkCounts[0],
+ outputBuffer,
+ outputBufferBytes,
+ outputBufferBytesUsed);
+ }
+ else if ((textureFormats[0] != HapTextureFormat_YCoCg_DXT5 && textureFormats[1] != HapTextureFormat_YCoCg_DXT5)
+ && (textureFormats[0] != HapTextureFormat_A_RGTC1 && textureFormats[1] != HapTextureFormat_A_RGTC1))
+ {
+ /*
+ Permitted combinations:
+ HapTextureFormat_YCoCg_DXT5 + HapTextureFormat_A_RGTC1
+ */
+ return HapResult_Bad_Arguments;
+ }
+ else
+ {
+ // Calculate the worst-case size for the top section and choose a header-length based on that
+ top_section_length = 0;
+ for (int i = 0; i < count; i++)
+ {
+ top_section_length += inputBuffersBytes[i] + hap_decode_instructions_length(chunkCounts[i]) + 4;
+ }
+
+ if (top_section_length > kHapUInt24Max)
+ {
+ top_section_header_length = 8U;
+ }
+ else
+ {
+ top_section_header_length = 4U;
+ }
+
+ // Encode each texture
+ top_section_length = 0;
+ for (int i = 0; i < count; i++)
+ {
+ void *section = ((uint8_t *)outputBuffer) + top_section_header_length + top_section_length;
+ unsigned int result = hap_encode_texture(inputBuffers[i],
+ inputBuffersBytes[i],
+ textureFormats[i],
+ compressors[i],
+ chunkCounts[i],
+ section,
+ outputBufferBytes - (top_section_header_length + top_section_length),
+ &section_length);
+ if (result != HapResult_No_Error)
+ {
+ return result;
+ }
+ top_section_length += section_length;
+ }
+
+ hap_write_section_header(outputBuffer, top_section_header_length, top_section_length, kHapSectionMultipleImages);
+
+ *outputBufferBytesUsed = top_section_length + top_section_header_length;
+
+ return HapResult_No_Error;
+ }
+}
+
+static void hap_decode_chunk(HapChunkDecodeInfo chunks[], unsigned int index)
+{
+ if (chunks)
+ {
+ if (chunks[index].compressor == kHapCompressorSnappy)
+ {
+ snappy_status snappy_result = snappy_uncompress(chunks[index].compressed_chunk_data,
+ chunks[index].compressed_chunk_size,
+ chunks[index].uncompressed_chunk_data,
+ &chunks[index].uncompressed_chunk_size);
+
+ switch (snappy_result)
+ {
+ case SNAPPY_INVALID_INPUT:
+ chunks[index].result = HapResult_Bad_Frame;
+ break;
+ case SNAPPY_OK:
+ chunks[index].result = HapResult_No_Error;
+ break;
+ default:
+ chunks[index].result = HapResult_Internal_Error;
+ break;
+ }
+ }
+ else if (chunks[index].compressor == kHapCompressorNone)
+ {
+ memcpy(chunks[index].uncompressed_chunk_data,
+ chunks[index].compressed_chunk_data,
+ chunks[index].compressed_chunk_size);
+ chunks[index].result = HapResult_No_Error;
+ }
+ else
+ {
+ chunks[index].result = HapResult_Bad_Frame;
+ }
+ }
+}
+
+static unsigned int hap_decode_header_complex_instructions(const void *texture_section, uint32_t texture_section_length, int * chunk_count,
+ const void **compressors, const void **chunk_sizes, const void **chunk_offsets, const char **frame_data){
+ int result = HapResult_No_Error;
+ const void *section_start;
+ uint32_t section_header_length;
+ uint32_t section_length;
+ unsigned int section_type;
+ size_t bytes_remaining = 0;
+
+ *compressors = NULL;
+ *chunk_sizes = NULL;
+ *chunk_offsets = NULL;
+
+ result = hap_read_section_header(texture_section, texture_section_length, &section_header_length, &section_length, &section_type);
+
+ if (result == HapResult_No_Error && section_type != kHapSectionDecodeInstructionsContainer)
+ {
+ result = HapResult_Bad_Frame;
+ }
+
+ if (result != HapResult_No_Error)
+ {
+ return result;
+ }
+
+ /*
+ Frame data follows immediately after the Decode Instructions Container
+ */
+ *frame_data = ((const char *)texture_section) + section_header_length + section_length;
+
+ /*
+ Step through the sections inside the Decode Instructions Container
+ */
+ section_start = ((uint8_t *)texture_section) + section_header_length;
+ bytes_remaining = section_length;
+
+ while (bytes_remaining > 0) {
+ unsigned int section_chunk_count = 0;
+ result = hap_read_section_header(section_start, bytes_remaining, &section_header_length, &section_length, &section_type);
+ if (result != HapResult_No_Error)
+ {
+ return result;
+ }
+ section_start = ((uint8_t *)section_start) + section_header_length;
+ switch (section_type) {
+ case kHapSectionChunkSecondStageCompressorTable:
+ *compressors = section_start;
+ section_chunk_count = section_length;
+ break;
+ case kHapSectionChunkSizeTable:
+ *chunk_sizes = section_start;
+ section_chunk_count = section_length / 4;
+ break;
+ case kHapSectionChunkOffsetTable:
+ *chunk_offsets = section_start;
+ section_chunk_count = section_length / 4;
+ break;
+ default:
+ // Ignore unrecognized sections
+ break;
+ }
+
+ /*
+ If we calculated a chunk count and already have one, make sure they match
+ */
+ if (section_chunk_count != 0)
+ {
+ if ((*chunk_count) != 0 && section_chunk_count != (*chunk_count))
+ {
+ return HapResult_Bad_Frame;
+ }
+ *chunk_count = section_chunk_count;
+ }
+
+ section_start = ((uint8_t *)section_start) + section_length;
+ bytes_remaining -= section_header_length + section_length;
+ }
+
+ /*
+ The Chunk Second-Stage Compressor Table and Chunk Size Table are required
+ */
+ if (*compressors == NULL || *chunk_sizes == NULL)
+ {
+ return HapResult_Bad_Frame;
+ }
+ return result;
+}
+
+unsigned int hap_decode_single_texture(const void *texture_section, uint32_t texture_section_length,
+ unsigned int texture_section_type,
+ HapDecodeCallback callback, void *info,
+ void *outputBuffer, unsigned long outputBufferBytes,
+ unsigned long *outputBufferBytesUsed,
+ unsigned int *outputBufferTextureFormat)
+{
+ int result = HapResult_No_Error;
+ unsigned int textureFormat;
+ unsigned int compressor;
+ size_t bytesUsed = 0;
+
+ /*
+ One top-level section type describes texture-format and second-stage compression
+ Hap compressor/format constants can be unpacked by reading the top and bottom four bits.
+ */
+ compressor = hap_top_4_bits(texture_section_type);
+ textureFormat = hap_bottom_4_bits(texture_section_type);
+
+ /*
+ Pass the texture format out
+ */
+ *outputBufferTextureFormat = hap_texture_format_constant_for_format_identifier(textureFormat);
+ if (*outputBufferTextureFormat == 0)
+ {
+ return HapResult_Bad_Frame;
+ }
+
+ if (compressor == kHapCompressorComplex)
+ {
+ /*
+ The top-level section should contain a Decode Instructions Container followed by frame data
+ */
+ int chunk_count = 0;
+ const void *compressors = NULL;
+ const void *chunk_sizes = NULL;
+ const void *chunk_offsets = NULL;
+ const char *frame_data = NULL;
+
+ result = hap_decode_header_complex_instructions(texture_section, texture_section_length, &chunk_count, &compressors, &chunk_sizes, &chunk_offsets, &frame_data);
+
+ if (result != HapResult_No_Error)
+ {
+ return result;
+ }
+
+ if (chunk_count > 0)
+ {
+ /*
+ Step through the chunks, storing information for their decompression
+ */
+ HapChunkDecodeInfo *chunk_info = (HapChunkDecodeInfo *)malloc(sizeof(HapChunkDecodeInfo) * chunk_count);
+
+ size_t running_compressed_chunk_size = 0;
+ size_t running_uncompressed_chunk_size = 0;
+ int i;
+
+ if (chunk_info == NULL)
+ {
+ return HapResult_Internal_Error;
+ }
+
+ for (i = 0; i < chunk_count; i++) {
+
+ chunk_info[i].compressor = *(((uint8_t *)compressors) + i);
+
+ chunk_info[i].compressed_chunk_size = hap_read_4_byte_uint(((uint8_t *)chunk_sizes) + (i * 4));
+
+ if (chunk_offsets)
+ {
+ chunk_info[i].compressed_chunk_data = frame_data + hap_read_4_byte_uint(((uint8_t *)chunk_offsets) + (i * 4));
+ }
+ else
+ {
+ chunk_info[i].compressed_chunk_data = frame_data + running_compressed_chunk_size;
+ }
+
+ running_compressed_chunk_size += chunk_info[i].compressed_chunk_size;
+
+ if (chunk_info[i].compressor == kHapCompressorSnappy)
+ {
+ snappy_status snappy_result = snappy_uncompressed_length(chunk_info[i].compressed_chunk_data,
+ chunk_info[i].compressed_chunk_size,
+ &(chunk_info[i].uncompressed_chunk_size));
+
+ if (snappy_result != SNAPPY_OK)
+ {
+ switch (snappy_result)
+ {
+ case SNAPPY_INVALID_INPUT:
+ result = HapResult_Bad_Frame;
+ break;
+ default:
+ result = HapResult_Internal_Error;
+ break;
+ }
+ break;
+ }
+ }
+ else
+ {
+ chunk_info[i].uncompressed_chunk_size = chunk_info[i].compressed_chunk_size;
+ }
+
+ chunk_info[i].uncompressed_chunk_data = (char *)(((uint8_t *)outputBuffer) + running_uncompressed_chunk_size);
+ running_uncompressed_chunk_size += chunk_info[i].uncompressed_chunk_size;
+ }
+
+ if (result == HapResult_No_Error && running_uncompressed_chunk_size > outputBufferBytes)
+ {
+ result = HapResult_Buffer_Too_Small;
+ }
+
+ if (result == HapResult_No_Error)
+ {
+ /*
+ Perform decompression
+ */
+ bytesUsed = running_uncompressed_chunk_size;
+
+ if (chunk_count == 1)
+ {
+ /*
+ We don't invoke the callback for one chunk, just decode it directly
+ */
+ hap_decode_chunk(chunk_info, 0);
+ }
+ else
+ {
+ callback((HapDecodeWorkFunction)hap_decode_chunk, chunk_info, chunk_count, info);
+ }
+
+ /*
+ Check to see if we encountered any errors and report one of them
+ */
+ for (i = 0; i < chunk_count; i++)
+ {
+ if (chunk_info[i].result != HapResult_No_Error)
+ {
+ result = chunk_info[i].result;
+ break;
+ }
+ }
+ }
+
+ free(chunk_info);
+
+ if (result != HapResult_No_Error)
+ {
+ return result;
+ }
+ }
+ }
+ else if (compressor == kHapCompressorSnappy)
+ {
+ /*
+ Only one section is present containing a single block of snappy-compressed texture data
+ */
+ snappy_status snappy_result = snappy_uncompressed_length((const char *)texture_section, texture_section_length, &bytesUsed);
+ if (snappy_result != SNAPPY_OK)
+ {
+ return HapResult_Internal_Error;
+ }
+ if (bytesUsed > outputBufferBytes)
+ {
+ return HapResult_Buffer_Too_Small;
+ }
+ snappy_result = snappy_uncompress((const char *)texture_section, texture_section_length, (char *)outputBuffer, &bytesUsed);
+ if (snappy_result != SNAPPY_OK)
+ {
+ return HapResult_Internal_Error;
+ }
+ }
+ else if (compressor == kHapCompressorNone)
+ {
+ /*
+ Only one section is present containing a single block of uncompressed texture data
+ */
+ bytesUsed = texture_section_length;
+ if (texture_section_length > outputBufferBytes)
+ {
+ return HapResult_Buffer_Too_Small;
+ }
+ memcpy(outputBuffer, texture_section, texture_section_length);
+ }
+ else
+ {
+ return HapResult_Bad_Frame;
+ }
+ /*
+ Fill out the remaining return value
+ */
+ if (outputBufferBytesUsed != NULL)
+ {
+ *outputBufferBytesUsed = bytesUsed;
+ }
+
+ return HapResult_No_Error;
+}
+
+int hap_get_section_at_index(const void *input_buffer, uint32_t input_buffer_bytes,
+ unsigned int index,
+ const void **section, uint32_t *section_length, unsigned int *section_type)
+{
+ int result;
+ uint32_t section_header_length;
+
+ result = hap_read_section_header(input_buffer, input_buffer_bytes, &section_header_length, section_length, section_type);
+
+ if (result != HapResult_No_Error)
+ {
+ return result;
+ }
+
+ if (*section_type == kHapSectionMultipleImages)
+ {
+ /*
+ Step through until we find the section at index
+ */
+ size_t offset = 0;
+ size_t top_section_length = *section_length;
+ input_buffer = ((uint8_t *)input_buffer) + section_header_length;
+ section_header_length = 0;
+ *section_length = 0;
+ for (int i = 0; i <= index; i++) {
+ offset += section_header_length + *section_length;
+ if (offset >= top_section_length)
+ {
+ return HapResult_Bad_Arguments;
+ }
+ result = hap_read_section_header(((uint8_t *)input_buffer) + offset,
+ top_section_length - offset,
+ &section_header_length,
+ section_length,
+ section_type);
+ if (result != HapResult_No_Error)
+ {
+ return result;
+ }
+ }
+ offset += section_header_length;
+ *section = ((uint8_t *)input_buffer) + offset;
+ return HapResult_No_Error;
+ }
+ else if (index == 0)
+ {
+ /*
+ A single-texture frame with the texture as the top section.
+ */
+ *section = ((uint8_t *)input_buffer) + section_header_length;
+ return HapResult_No_Error;
+ }
+ else
+ {
+ *section = NULL;
+ *section_length = 0;
+ *section_type = 0;
+ return HapResult_Bad_Arguments;
+ }
+}
+
+unsigned int HapDecode(const void *inputBuffer, unsigned long inputBufferBytes,
+ unsigned int index,
+ HapDecodeCallback callback, void *info,
+ void *outputBuffer, unsigned long outputBufferBytes,
+ unsigned long *outputBufferBytesUsed,
+ unsigned int *outputBufferTextureFormat)
+{
+ int result = HapResult_No_Error;
+ const void *section;
+ uint32_t section_length;
+ unsigned int section_type;
+
+ /*
+ Check arguments
+ */
+ if (inputBuffer == NULL
+ || index > 1
+ || callback == NULL
+ || outputBuffer == NULL
+ || outputBufferTextureFormat == NULL
+ )
+ {
+ return HapResult_Bad_Arguments;
+ }
+
+ /*
+ Locate the section at the given index, which will either be the top-level section in a single texture image, or one of the
+ sections inside a multi-image top-level section.
+ */
+ result = hap_get_section_at_index(inputBuffer, inputBufferBytes, index, &section, &section_length, &section_type);
+
+ if (result == HapResult_No_Error)
+ {
+ /*
+ Decode the located texture
+ */
+ result = hap_decode_single_texture(section,
+ section_length,
+ section_type,
+ callback, info,
+ outputBuffer,
+ outputBufferBytes,
+ outputBufferBytesUsed,
+ outputBufferTextureFormat);
+ }
+
+ return result;
+}
+
+unsigned int HapGetFrameTextureCount(const void *inputBuffer, unsigned long inputBufferBytes, unsigned int *outputTextureCount)
+{
+ int result;
+ uint32_t section_header_length;
+ uint32_t section_length;
+ unsigned int section_type;
+
+ result = hap_read_section_header(inputBuffer, inputBufferBytes, &section_header_length, &section_length, &section_type);
+
+ if (result != HapResult_No_Error)
+ {
+ return result;
+ }
+
+ if (section_type == kHapSectionMultipleImages)
+ {
+ /*
+ Step through, counting sections
+ */
+ uint32_t offset = section_header_length;
+ uint32_t top_section_length = section_length;
+ *outputTextureCount = 0;
+ while (offset < top_section_length) {
+ result = hap_read_section_header(((uint8_t *)inputBuffer) + offset,
+ inputBufferBytes - offset,
+ &section_header_length,
+ &section_length,
+ &section_type);
+ if (result != HapResult_No_Error)
+ {
+ return result;
+ }
+ offset += section_header_length + section_length;
+ *outputTextureCount += 1;
+ }
+ return HapResult_No_Error;
+ }
+ else
+ {
+ /*
+ A single-texture frame with the texture as the top section.
+ */
+ *outputTextureCount = 1;
+ return HapResult_No_Error;
+ }
+}
+
+unsigned int HapGetFrameTextureFormat(const void *inputBuffer, unsigned long inputBufferBytes, unsigned int index, unsigned int *outputBufferTextureFormat)
+{
+ unsigned int result = HapResult_No_Error;
+ const void *section;
+ uint32_t section_length;
+ unsigned int section_type;
+ /*
+ Check arguments
+ */
+ if (inputBuffer == NULL
+ || index > 1
+ || outputBufferTextureFormat == NULL
+ )
+ {
+ return HapResult_Bad_Arguments;
+ }
+ /*
+ Locate the section at the given index, which will either be the top-level section in a single texture image, or one of the
+ sections inside a multi-image top-level section.
+ */
+ result = hap_get_section_at_index(inputBuffer, inputBufferBytes, index, &section, &section_length, &section_type);
+
+ if (result == HapResult_No_Error)
+ {
+ /*
+ Pass the API enum value to match the constant out
+ */
+ *outputBufferTextureFormat = hap_texture_format_constant_for_format_identifier(hap_bottom_4_bits(section_type));
+ /*
+ Check a valid format was present
+ */
+ if (*outputBufferTextureFormat == 0)
+ {
+ result = HapResult_Bad_Frame;
+ }
+ }
+ return result;
+}
+
+unsigned int HapGetFrameTextureChunkCount(const void *inputBuffer, unsigned long inputBufferBytes, unsigned int index, int *chunk_count)
+{
+ unsigned int result = HapResult_No_Error;
+ const void *section;
+ uint32_t section_length;
+ unsigned int section_type;
+ *chunk_count = 0;
+
+ /*
+ Check arguments
+ */
+ if (inputBuffer == NULL
+ || index > 1
+ )
+ {
+ return HapResult_Bad_Arguments;
+ }
+ /*
+ Locate the section at the given index, which will either be the top-level section in a single texture image, or one of the
+ sections inside a multi-image top-level section.
+ */
+ result = hap_get_section_at_index(inputBuffer, inputBufferBytes, index, &section, &section_length, &section_type);
+
+ if (result == HapResult_No_Error)
+ {
+ unsigned int compressor;
+
+ /*
+ One top-level section type describes texture-format and second-stage compression
+ Hap compressor/format constants can be unpacked by reading the top and bottom four bits.
+ */
+ compressor = hap_top_4_bits(section_type);
+
+ if (compressor == kHapCompressorComplex)
+ {
+ /*
+ The top-level section should contain a Decode Instructions Container followed by frame data
+ */
+ const void *compressors = NULL;
+ const void *chunk_sizes = NULL;
+ const void *chunk_offsets = NULL;
+ const char *frame_data = NULL;
+
+ result = hap_decode_header_complex_instructions(section, section_length, chunk_count, &compressors, &chunk_sizes, &chunk_offsets, &frame_data);
+
+ if (result != HapResult_No_Error)
+ {
+ return result;
+ }
+ }
+ else if ((compressor == kHapCompressorSnappy)||(compressor == kHapCompressorNone))
+ {
+ *chunk_count = 1;
+ }
+ else
+ {
+ return HapResult_Bad_Frame;
+ }
+ }
+ return result;
+}
diff --git a/lib/hap.h b/lib/hap.h
new file mode 100644
index 0000000..4f7dd8b
--- /dev/null
+++ b/lib/hap.h
@@ -0,0 +1,158 @@
+/*
+ hap.h
+
+ Copyright (c) 2011-2013, Tom Butterworth and Vidvox LLC. All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef hap_h
+#define hap_h
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ These match the constants defined by GL_EXT_texture_compression_s3tc,
+ GL_ARB_texture_compression_rgtc and GL_ARB_texture_compression_bptc
+ */
+
+enum HapTextureFormat {
+ HapTextureFormat_RGB_DXT1 = 0x83F0,
+ HapTextureFormat_RGBA_DXT5 = 0x83F3,
+ HapTextureFormat_YCoCg_DXT5 = 0x01,
+ HapTextureFormat_A_RGTC1 = 0x8DBB,
+ HapTextureFormat_RGBA_BPTC_UNORM = 0x8E8C,
+ HapTextureFormat_RGB_BPTC_UNSIGNED_FLOAT = 0x8E8F,
+ HapTextureFormat_RGB_BPTC_SIGNED_FLOAT = 0x8E8E,
+};
+
+enum HapCompressor {
+ HapCompressorNone,
+ HapCompressorSnappy
+};
+
+enum HapResult {
+ HapResult_No_Error = 0,
+ HapResult_Bad_Arguments,
+ HapResult_Buffer_Too_Small,
+ HapResult_Bad_Frame,
+ HapResult_Internal_Error
+};
+
+/*
+ See HapDecode for descriptions of these function types.
+ */
+typedef void (*HapDecodeWorkFunction)(void *p, unsigned int index);
+typedef void (*HapDecodeCallback)(HapDecodeWorkFunction function, void *p, unsigned int count, void *info);
+
+/*
+ Returns the maximum size of an output buffer for a frame composed of one or more textures, or returns 0 on error.
+ count is the number of textures (1 or 2) and matches the number of values in the array arguments
+ lengths is an array of input texture lengths in bytes
+ textureFormats is an array of HapTextureFormats
+ chunkCounts is an array of chunk counts (1 or more)
+ */
+unsigned long HapMaxEncodedLength(unsigned int count,
+ unsigned long *lengths,
+ unsigned int *textureFormats,
+ unsigned int *chunkCounts);
+
+/*
+ Encodes one or multiple textures into one Hap frame, or returns an error.
+
+ Permitted multiple-texture combinations are:
+ HapTextureFormat_YCoCg_DXT5 + HapTextureFormat_A_RGTC1
+
+ Use HapMaxEncodedLength() to discover the minimal value for outputBufferBytes.
+ count is the number of textures (1 or 2) and matches the number of values in the array arguments
+ inputBuffers is an array of count pointers to texture data
+ inputBufferBytes is an array of texture data lengths in bytes
+ textureFormats is an array of HapTextureFormats
+ compressors is an array of HapCompressors
+ chunkCounts is an array of chunk counts to permit multithreaded decoding (1 or more)
+ outputBuffer is the destination buffer to receive the encoded frame
+ outputBufferBytes is the destination buffer's length in bytes
+ outputBufferBytesUsed will be set to the actual encoded length of the frame on return
+*/
+unsigned int HapEncode(unsigned int count,
+ const void **inputBuffers, unsigned long *inputBuffersBytes,
+ unsigned int *textureFormats,
+ unsigned int *compressors,
+ unsigned int *chunkCounts,
+ void *outputBuffer, unsigned long outputBufferBytes,
+ unsigned long *outputBufferBytesUsed);
+
+/*
+ Decodes a texture from inputBuffer which is a Hap frame.
+
+ A frame may contain multiple textures which are to be combined to create the final image. Use HapGetFrameTextureCount()
+ to discover the number of textures in a frame, and then access each texture by incrementing the index argument to this
+ function.
+
+ If the frame permits multithreaded decoding, callback will be called once for you to invoke a platform-appropriate
+ mechanism to assign work to threads, and trigger that work by calling the function passed to your callback the number
+ of times indicated by the count argument, usually from a number of different threads. This callback must not return
+ until all the work has been completed.
+
+ void MyHapDecodeCallback(HapDecodeWorkFunction function, void *p, unsigned int count, void *info)
+ {
+ int i;
+ for (i = 0; i < count; i++) {
+ // Invoke your multithreading mechanism to cause this function to be called
+ // on a suitable number of threads.
+ function(p, i);
+ }
+ }
+ info is an argument for your own use to pass context to the callback.
+ If the frame does not permit multithreaded decoding, callback will not be called.
+ If outputBufferBytesUsed is not NULL then it will be set to the decoded length of the output buffer.
+ outputBufferTextureFormat must be non-NULL, and will be set to one of the HapTextureFormat constants.
+ */
+unsigned int HapDecode(const void *inputBuffer, unsigned long inputBufferBytes,
+ unsigned int index,
+ HapDecodeCallback callback, void *info,
+ void *outputBuffer, unsigned long outputBufferBytes,
+ unsigned long *outputBufferBytesUsed,
+ unsigned int *outputBufferTextureFormat);
+
+/*
+ If this returns HapResult_No_Error then outputTextureCount is set to the count of textures in the frame.
+ */
+unsigned int HapGetFrameTextureCount(const void *inputBuffer, unsigned long inputBufferBytes, unsigned int *outputTextureCount);
+
+/*
+ On return sets outputBufferTextureFormat to a HapTextureFormat constant describing the format of the texture at index in the frame.
+ */
+unsigned int HapGetFrameTextureFormat(const void *inputBuffer, unsigned long inputBufferBytes, unsigned int index, unsigned int *outputBufferTextureFormat);
+
+/*
+ On return sets chunk_count to the chunk count value of the texture at index in the frame.
+*/
+unsigned int HapGetFrameTextureChunkCount(const void *inputBuffer, unsigned long inputBufferBytes, unsigned int index, int *chunk_count);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/c.zig b/src/c.zig
index 7b46914..54c156e 100644
--- a/src/c.zig
+++ b/src/c.zig
@@ -11,4 +11,8 @@ pub usingnamespace @cImport({
@cInclude("libavformat/avformat.h");
@cInclude("libswscale/swscale.h");
}
+
+ if (build_config.have_hap) {
+ @cInclude("hap.h");
+ }
});
diff --git a/src/control.zig b/src/control.zig
index 0449b3d..dfdd55d 100644
--- a/src/control.zig
+++ b/src/control.zig
@@ -108,8 +108,7 @@ fn set_texture(
const filepath = try project_root.realpathZ(@ptrCast(&argv[0].*.s), buffer[0..]);
buffer[filepath.len] = 0;
- dest.* = gl.Texture.create(texture_type);
- try video.loadVideo(progress, @ptrCast(filepath), &dest.*.?);
+ dest.* = try video.loadVideo(progress, @ptrCast(filepath), texture_type);
}
pub const ControlServer = struct {
diff --git a/src/gl.zig b/src/gl.zig
index c1543dc..22a2ebe 100644
--- a/src/gl.zig
+++ b/src/gl.zig
@@ -73,54 +73,95 @@ pub const Texture = struct {
c.glDeleteTextures(1, &self.id);
}
- pub fn setData2D(self: *const Texture, width: i32, height: i32, data: [*]const u8) void {
+ pub fn allocate(self: *const Texture, width: i32, height: i32, depth: i32, format: c.GLenum) void {
c.glBindTexture(@intFromEnum(self.type), self.id);
defer c.glBindTexture(@intFromEnum(self.type), 0);
- c.glTexImage2D(
- @intFromEnum(self.type),
- 0,
- c.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, // c.GL_RGBA,
- width,
- height,
- 0,
- c.GL_RGBA,
- c.GL_UNSIGNED_BYTE,
- data,
- );
+ switch (self.type) {
+ .TEXTURE_2D => c.glTexStorage2D(
+ @intFromEnum(self.type),
+ 1,
+ format,
+ width,
+ height,
+ ),
+
+ .TEXTURE_2D_ARRAY, .TEXTURE_3D => c.glTexStorage3D(
+ @intFromEnum(self.type),
+ 1,
+ format,
+ width,
+ height,
+ depth,
+ ),
+ else => unreachable,
+ }
}
- pub fn allocate3D(self: *const Texture, width: i32, height: i32, depth: i32) void {
+ pub fn setLayer(self: *const Texture, width: i32, height: i32, layer: i32, format: c.GLenum, data: [*]const u8) void {
c.glBindTexture(@intFromEnum(self.type), self.id);
defer c.glBindTexture(@intFromEnum(self.type), 0);
- c.glTexStorage3D(
- @intFromEnum(self.type),
- 1,
- c.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT, // c.GL_RGBA,
- width,
- height,
- depth,
- );
+ switch (self.type) {
+ .TEXTURE_2D => c.glTexSubImage2D(
+ @intFromEnum(self.type),
+ 0,
+ 0,
+ 0,
+ width,
+ height,
+ format,
+ c.GL_UNSIGNED_BYTE,
+ data,
+ ),
+ .TEXTURE_2D_ARRAY, .TEXTURE_3D => c.glTexSubImage3D(
+ @intFromEnum(self.type),
+ 0,
+ 0,
+ 0,
+ layer,
+ width,
+ height,
+ 1,
+ format,
+ c.GL_UNSIGNED_BYTE,
+ data,
+ ),
+ else => unreachable,
+ }
}
- pub fn setLayer3D(self: *const Texture, width: i32, height: i32, layer: i32, data: [*]const u8) void {
+ pub fn setLayerCompressed(self: *const Texture, width: i32, height: i32, layer: i32, format: c.GLenum, data: []const u8) void {
c.glBindTexture(@intFromEnum(self.type), self.id);
defer c.glBindTexture(@intFromEnum(self.type), 0);
- c.glTexSubImage3D(
- @intFromEnum(self.type),
- 0,
- 0,
- 0,
- layer,
- width,
- height,
- 1,
- c.GL_RGBA,
- c.GL_UNSIGNED_BYTE,
- data,
- );
+ switch (self.type) {
+ .TEXTURE_2D => c.glCompressedTexSubImage2D(
+ @intFromEnum(self.type),
+ 0,
+ 0,
+ 0,
+ width,
+ height,
+ format,
+ @intCast(data.len),
+ data.ptr,
+ ),
+ .TEXTURE_2D_ARRAY, .TEXTURE_3D => c.glCompressedTexSubImage3D(
+ @intFromEnum(self.type),
+ 0,
+ 0,
+ 0,
+ layer,
+ width,
+ height,
+ 1,
+ format,
+ @intCast(data.len),
+ data.ptr,
+ ),
+ else => unreachable,
+ }
}
};
diff --git a/src/video.zig b/src/video.zig
index 368d46f..98772b6 100644
--- a/src/video.zig
+++ b/src/video.zig
@@ -2,40 +2,133 @@ const std = @import("std");
const c = @import("c.zig");
const gl = @import("gl.zig");
-fn check(err: c_int) !void {
+pub const Errors = error{
+ AVGenericError,
+ AVAllocationError,
+ HAPBadArguments,
+ HAPBufferTooSmall,
+ HAPBadFrame,
+ HAPInternalError,
+ UnsupportedCodec,
+ OutOfMemory,
+ NoFrames,
+};
+
+fn check(err: c_int) Errors!void {
if (err >= 0) return;
var buf: [c.AV_ERROR_MAX_STRING_SIZE]u8 = undefined;
_ = c.av_make_error_string(&buf, buf.len, err);
std.debug.print("libav error: {s}\n", .{buf});
- return error.AVError;
+ return error.AVGenericError;
}
-pub fn loadVideo(progress_root: std.Progress.Node, filename: [*:0]const u8, texture: *gl.Texture) !void {
- const outer_progress = progress_root.start("load video", 0);
- defer outer_progress.end();
+fn hapCheck(err: c_uint) Errors!void {
+ return switch (err) {
+ c.HapResult_No_Error => return,
+ c.HapResult_Bad_Arguments => return error.HAPBadArguments,
+ c.HapResult_Buffer_Too_Small => return error.HAPBufferTooSmall,
+ c.HapResult_Bad_Frame => return error.HAPBadFrame,
+ c.HapResult_Internal_Error => return error.HAPInternalError,
+ else => unreachable,
+ };
+}
- var format_ctx: ?*c.AVFormatContext = null;
- try check(c.avformat_open_input(&format_ctx, filename, null, null));
- defer c.avformat_close_input(&format_ctx);
+const Decoder = struct {
+ num_frames: i32,
- if (format_ctx) |format| {
- try check(c.avformat_find_stream_info(format, null));
+ process_packet_fn: *const fn (self: *Decoder, packet: *c.AVPacket, texture: ?*const gl.Texture) Errors!bool,
+ init_texture_fn: *const fn (self: *Decoder, texture: *const gl.Texture) void,
+ reset_fn: ?*const fn (self: *Decoder) void,
+ deinit_fn: *const fn (self: *Decoder, allocator: std.mem.Allocator) void,
- const video_stream = for (format.streams, 0..format.nb_streams) |c_stream, _| {
- const stream = @as(*c.AVStream, c_stream orelse unreachable);
- if (stream.codecpar.*.codec_type == c.AVMEDIA_TYPE_VIDEO) break stream;
- } else unreachable;
+ fn processStream(
+ self: *Decoder,
+ progress: std.Progress.Node,
+ format: *c.AVFormatContext,
+ stream: *c.AVStream,
+ texture: ?*const gl.Texture,
+ ) Errors!void {
+ defer progress.end();
- const codec_par = video_stream.codecpar.*;
- const codec = c.avcodec_find_decoder(codec_par.codec_id) orelse unreachable;
- var codec_ctx = c.avcodec_alloc_context3(codec) orelse unreachable;
- defer c.avcodec_free_context(&codec_ctx);
+ try check(c.avformat_seek_file(format, stream.index, 0, 0, 0, 0));
+ if (self.reset_fn) |reset_fn| reset_fn(self);
+
+ var packet = c.av_packet_alloc();
+ defer c.av_packet_free(&packet);
+
+ self.num_frames = 0;
+ var res = c.av_read_frame(format, packet);
+ while (res >= 0) : (res = c.av_read_frame(format, packet)) {
+ if (packet.*.stream_index == stream.index) {
+ const more = try self.process_packet_fn(self, packet, texture);
+ progress.setCompletedItems(@intCast(self.num_frames));
+ if (!more) break;
+ }
+ c.av_packet_unref(packet);
+ } else {
+ if (res != c.AVERROR_EOF) try check(res);
+ }
+ }
+
+ pub fn createTexture(
+ self: *Decoder,
+ outer_progress: std.Progress.Node,
+ format: *c.AVFormatContext,
+ stream: *c.AVStream,
+ texture_type: gl.Texture.Type,
+ ) Errors!gl.Texture {
+ try self.processStream(
+ outer_progress.start("scanning video frames", @intCast(stream.nb_frames)),
+ format,
+ stream,
+ null,
+ );
+ if (self.num_frames <= 0) return error.NoFrames;
+
+ const texture = gl.Texture.create(texture_type);
+ errdefer texture.destroy();
+
+ self.init_texture_fn(self, &texture);
+ try self.processStream(
+ outer_progress.start("loading video frames", @intCast(self.num_frames)),
+ format,
+ stream,
+ &texture,
+ );
+ return texture;
+ }
+
+ pub fn deinit(self: *Decoder, allocator: std.mem.Allocator) void {
+ self.deinit_fn(self, allocator);
+ }
+};
+
+const AVDecoder = struct {
+ decoder: Decoder,
+
+ sws_ctx: *c.SwsContext,
+ codec_par: c.AVCodecParameters,
+ codec_ctx: [*c]c.AVCodecContext,
+
+ raw_frame: [*c]c.AVFrame,
+ rgba_frame: [*c]c.AVFrame,
+
+ pub fn init(allocator: std.mem.Allocator, codec_par: c.AVCodecParameters) Errors!*Decoder {
+ const self = try allocator.create(AVDecoder);
+ errdefer allocator.destroy(self);
+
+ const codec = c.avcodec_find_decoder(codec_par.codec_id) orelse return error.UnsupportedCodec;
+
+ var codec_ctx = c.avcodec_alloc_context3(codec) orelse return error.AVAllocationError;
+ errdefer c.avcodec_free_context(&codec_ctx);
+ var raw_frame = c.av_frame_alloc() orelse return error.AVAllocationError;
+ errdefer c.av_frame_free(&raw_frame);
+ var rgba_frame = c.av_frame_alloc() orelse return error.AVAllocationError;
+ errdefer c.av_frame_free(&rgba_frame);
try check(c.avcodec_parameters_to_context(codec_ctx, &codec_par));
try check(c.avcodec_open2(codec_ctx, codec, null));
- defer check(c.avcodec_close(codec_ctx)) catch unreachable;
-
- std.debug.print("opened codec, size {}x{}, ~{} frames\n", .{ codec_par.width, codec_par.height, video_stream.nb_frames });
+ errdefer check(c.avcodec_close(codec_ctx)) catch unreachable;
const sws_ctx = c.sws_getContext(
codec_par.width,
@@ -48,91 +141,229 @@ pub fn loadVideo(progress_root: std.Progress.Node, filename: [*:0]const u8, text
null,
null,
0,
- ) orelse unreachable;
- defer c.sws_freeContext(sws_ctx);
+ ) orelse return error.AVGenericError;
+ errdefer c.sws_freeContext(sws_ctx);
- var packet = c.av_packet_alloc();
- defer c.av_packet_free(&packet);
+ self.* = .{
+ .decoder = .{
+ .process_packet_fn = processPacket,
+ .init_texture_fn = initTexture,
+ .reset_fn = reset,
+ .deinit_fn = deinit,
+ .num_frames = 0,
+ },
+ .sws_ctx = sws_ctx,
+ .codec_par = codec_par,
+ .codec_ctx = codec_ctx,
+ .raw_frame = raw_frame,
+ .rgba_frame = rgba_frame,
+ };
+ return &self.decoder;
+ }
+
+ pub fn deinit(decoder: *Decoder, allocator: std.mem.Allocator) void {
+ const self: *AVDecoder = @fieldParentPtr("decoder", decoder);
+
+ const raw_ptr: [*c][*c]c.AVFrame = &self.raw_frame;
+ const rgba_ptr: [*c][*c]c.AVFrame = &self.rgba_frame;
+ c.av_frame_free(raw_ptr);
+ c.av_frame_free(rgba_ptr);
+
+ c.sws_freeContext(self.sws_ctx);
+ check(c.avcodec_close(self.codec_ctx)) catch unreachable;
+ c.avcodec_free_context(&self.codec_ctx);
+
+ allocator.destroy(self);
+ }
+
+ pub fn reset(decoder: *Decoder) void {
+ const self: *AVDecoder = @fieldParentPtr("decoder", decoder);
+ c.avcodec_flush_buffers(self.codec_ctx);
+ }
- var raw_frame = c.av_frame_alloc();
- defer c.av_frame_free(&raw_frame);
- var rgba_frame = c.av_frame_alloc();
- defer c.av_frame_free(&rgba_frame);
-
- const frame_count = blk: {
- const progress = outer_progress.start("scanning video frames", @intCast(video_stream.nb_frames));
- defer progress.end();
-
- var i: usize = 0;
- var res = c.av_read_frame(format, packet);
- while (res >= 0) : (res = c.av_read_frame(format, packet)) {
- if (packet.*.stream_index == video_stream.index) {
- try check(c.avcodec_send_packet(codec_ctx, packet));
-
- const ires = c.avcodec_receive_frame(codec_ctx, raw_frame);
- if (ires == c.AVERROR_EOF) break;
- if (ires == c.AVERROR(c.EAGAIN)) continue;
- try check(ires);
-
- i += 1;
- progress.completeOne();
- }
- c.av_packet_unref(packet);
+ pub fn initTexture(decoder: *Decoder, texture: *const gl.Texture) void {
+ const self: *AVDecoder = @fieldParentPtr("decoder", decoder);
+
+ texture.allocate(
+ self.codec_par.width,
+ self.codec_par.height,
+ self.decoder.num_frames,
+ c.GL_COMPRESSED_RGBA_S3TC_DXT1_EXT,
+ );
+ }
+
+ pub fn processPacket(decoder: *Decoder, packet: *c.AVPacket, texture: ?*const gl.Texture) Errors!bool {
+ const self: *AVDecoder = @fieldParentPtr("decoder", decoder);
+
+ try check(c.avcodec_send_packet(self.codec_ctx, packet));
+ while (true) {
+ const ires = c.avcodec_receive_frame(self.codec_ctx, self.raw_frame);
+ if (ires == c.AVERROR_EOF) return false;
+ if (ires == c.AVERROR(c.EAGAIN)) return true;
+ try check(ires);
+ try check(c.sws_scale_frame(self.sws_ctx, self.rgba_frame, self.raw_frame));
+
+ c.glPixelStorei(c.GL_UNPACK_ROW_LENGTH, @divFloor(self.rgba_frame.*.linesize[0], @sizeOf(u8) * 4));
+
+ if (texture) |tex| {
+ tex.setLayer(
+ self.rgba_frame.*.width,
+ self.rgba_frame.*.height,
+ self.decoder.num_frames,
+ c.GL_RGBA,
+ self.rgba_frame.*.data[0],
+ );
+
+ if (tex.type == .TEXTURE_2D) return false;
}
- try check(c.avformat_seek_file(format, video_stream.index, 0, 0, 0, 0));
- c.avcodec_flush_buffers(codec_ctx);
+ self.decoder.num_frames += 1;
+ }
+ }
+};
+
+const HAPDecoder = struct {
+ decoder: Decoder,
+
+ codec_par: c.AVCodecParameters,
+ format: ?c.HapTextureFormat,
+ gl_format: c.GLenum,
+
+ pub fn init(allocator: std.mem.Allocator, codec_par: c.AVCodecParameters) Errors!*Decoder {
+ const self = try allocator.create(HAPDecoder);
- break :blk i;
+ self.* = .{
+ .decoder = .{
+ .process_packet_fn = processPacket,
+ .init_texture_fn = initTexture,
+ .reset_fn = null,
+ .deinit_fn = deinit,
+ .num_frames = 0,
+ },
+ .codec_par = codec_par,
+ .format = null,
+ .gl_format = undefined,
};
+ return &self.decoder;
+ }
- std.debug.print("counted frames, found {}\n", .{frame_count});
- switch (texture.type) {
- .TEXTURE_2D => {},
- .TEXTURE_3D, .TEXTURE_2D_ARRAY => texture.allocate3D(codec_par.width, codec_par.height, @intCast(frame_count)),
- else => unreachable,
- }
+ pub fn deinit(decoder: *Decoder, allocator: std.mem.Allocator) void {
+ const self: *HAPDecoder = @fieldParentPtr("decoder", decoder);
+ allocator.destroy(self);
+ }
- {
- const progress = outer_progress.start("loading video frames", frame_count);
- defer progress.end();
+ pub fn initTexture(decoder: *Decoder, texture: *const gl.Texture) void {
+ const self: *HAPDecoder = @fieldParentPtr("decoder", decoder);
- defer c.glPixelStorei(c.GL_UNPACK_ROW_LENGTH, 0);
+ texture.allocate(
+ self.codec_par.width,
+ self.codec_par.height,
+ self.decoder.num_frames,
+ self.gl_format,
+ );
+ }
- var i: i32 = 0;
- var res = c.av_read_frame(format, packet);
- while (res >= 0) : (res = c.av_read_frame(format, packet)) {
- if (packet.*.stream_index == video_stream.index) {
- try check(c.avcodec_send_packet(codec_ctx, packet));
+ fn launchThread(workfn: c.HapDecodeWorkFunction, p: ?*anyopaque, count: c_uint, info: ?*anyopaque) callconv(.C) void {
+ _ = info;
- const ires = c.avcodec_receive_frame(codec_ctx, raw_frame);
- if (ires == c.AVERROR_EOF) break;
- if (ires == c.AVERROR(c.EAGAIN)) continue;
- try check(ires);
- try check(c.sws_scale_frame(sws_ctx, rgba_frame, raw_frame));
+ var i: c_uint = 0;
+ while (i < count) : (i += 1) {
+ workfn.?(p, i);
+ }
+ }
- c.glPixelStorei(c.GL_UNPACK_ROW_LENGTH, @divFloor(rgba_frame.*.linesize[0], @sizeOf(u8) * 4));
+ pub fn processPacket(decoder: *Decoder, packet: *c.AVPacket, texture: ?*const gl.Texture) Errors!bool {
+ const self: *HAPDecoder = @fieldParentPtr("decoder", decoder);
- switch (texture.type) {
- .TEXTURE_2D => {
- texture.setData2D(codec_par.width, codec_par.height, rgba_frame.*.data[0]);
- break;
- },
+ var textureCount: u32 = undefined;
+ try hapCheck(c.HapGetFrameTextureCount(packet.data, @intCast(packet.size), &textureCount));
+ if (textureCount != 1) {
+ return error.UnsupportedCodec;
+ }
- .TEXTURE_3D,
- .TEXTURE_2D_ARRAY,
- => texture.setLayer3D(codec_par.width, codec_par.height, i, rgba_frame.*.data[0]),
+ var format: c.HapTextureFormat = undefined;
- else => unreachable,
- }
+ if (texture) |tex| {
+ var buffer: [16 * 1024 * 1024]u8 = undefined;
+ var length: c_ulong = undefined;
+ try hapCheck(c.HapDecode(
+ packet.data,
+ @intCast(packet.size),
+ 0,
+ launchThread,
+ self,
+ &buffer,
+ buffer.len,
+ &length,
+ &format,
+ ));
- i += 1;
- progress.completeOne();
- }
- c.av_packet_unref(packet);
+ tex.setLayerCompressed(
+ self.codec_par.width,
+ self.codec_par.height,
+ self.decoder.num_frames,
+ self.gl_format,
+ buffer[0..length],
+ );
+
+ if (tex.type == .TEXTURE_2D) return false;
+ } else {
+ try hapCheck(c.HapGetFrameTextureFormat(packet.data, @intCast(packet.size), 0, &format));
+ if (self.format) |fmt| {
+ if (fmt != format) return error.UnsupportedCodec;
} else {
- if (res != c.AVERROR_EOF) try check(res);
+ self.format = format;
+ self.gl_format = switch (format) {
+ c.HapTextureFormat_RGB_DXT1 => c.GL_COMPRESSED_RGB_S3TC_DXT1_EXT,
+ c.HapTextureFormat_RGBA_DXT5 => c.GL_COMPRESSED_RGBA_S3TC_DXT5_EXT,
+ c.HapTextureFormat_YCoCg_DXT5,
+ c.HapTextureFormat_A_RGTC1,
+ c.HapTextureFormat_RGBA_BPTC_UNORM,
+ c.HapTextureFormat_RGB_BPTC_UNSIGNED_FLOAT,
+ c.HapTextureFormat_RGB_BPTC_SIGNED_FLOAT,
+ => return error.UnsupportedCodec,
+ else => unreachable,
+ };
}
}
+
+ self.decoder.num_frames += 1;
+ return true;
+ }
+};
+
+pub fn loadVideo(progress_root: std.Progress.Node, filename: [*:0]const u8, texture_type: gl.Texture.Type) !gl.Texture {
+ const progress = progress_root.start("loading texture", 2);
+ defer progress.end();
+
+ var format_ctx: ?*c.AVFormatContext = null;
+ try check(c.avformat_open_input(&format_ctx, filename, null, null));
+ defer c.avformat_close_input(&format_ctx);
+
+ if (format_ctx) |format| {
+ try check(c.avformat_find_stream_info(format, null));
+
+ const video_stream = for (format.streams, 0..format.nb_streams) |c_stream, _| {
+ const stream = @as(*c.AVStream, c_stream orelse unreachable);
+ if (stream.codecpar.*.codec_type == c.AVMEDIA_TYPE_VIDEO) break stream;
+ } else unreachable;
+
+ const codec_par = video_stream.codecpar.*;
+ std.debug.print("loading codec, size {}x{}, ~{} frames\n", .{ codec_par.width, codec_par.height, video_stream.nb_frames });
+
+ var packet = c.av_packet_alloc();
+ defer c.av_packet_free(&packet);
+
+ const allocator = std.heap.c_allocator;
+
+ const decoder = switch (codec_par.codec_tag) {
+ c.MKTAG('H', 'a', 'p', '1'),
+ c.MKTAG('H', 'a', 'p', '5'),
+ => try HAPDecoder.init(allocator, codec_par),
+ else => try AVDecoder.init(allocator, codec_par),
+ };
+ defer decoder.deinit(allocator);
+
+ return try decoder.createTexture(progress, format, video_stream, texture_type);
} else unreachable;
}