From 4a0802639c064ec908dc3e264c55c8db0fd6f00c Mon Sep 17 00:00:00 2001 From: Ali Chraghi <63465728+alichraghi@users.noreply.github.com> Date: Thu, 25 Aug 2022 15:26:06 +0430 Subject: [PATCH] basisu: add bindings for basis-universal (supercompressed textures) (#477) --- .gitmodules | 3 + basisu/.gitmodules | 3 + basisu/build.zig | 124 ++++++++++++++ basisu/src/encoder.zig | 259 ++++++++++++++++++++++++++++++ basisu/src/encoder/binding.zig | 42 +++++ basisu/src/encoder/wrapper.cpp | 176 ++++++++++++++++++++ basisu/src/main.zig | 45 ++++++ basisu/src/transcoder.zig | 229 ++++++++++++++++++++++++++ basisu/src/transcoder/binding.zig | 38 +++++ basisu/src/transcoder/wrapper.cpp | 153 ++++++++++++++++++ basisu/test/ziggy.png | Bin 0 -> 31849 bytes basisu/upstream | 1 + build.zig | 2 + 13 files changed, 1075 insertions(+) create mode 100644 basisu/.gitmodules create mode 100644 basisu/build.zig create mode 100644 basisu/src/encoder.zig create mode 100644 basisu/src/encoder/binding.zig create mode 100644 basisu/src/encoder/wrapper.cpp create mode 100644 basisu/src/main.zig create mode 100644 basisu/src/transcoder.zig create mode 100644 basisu/src/transcoder/binding.zig create mode 100644 basisu/src/transcoder/wrapper.cpp create mode 100644 basisu/test/ziggy.png create mode 160000 basisu/upstream diff --git a/.gitmodules b/.gitmodules index 3f522ce0..cab30a90 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,6 +28,9 @@ [submodule "sysaudio/upstream"] path = sysaudio/upstream url = https://github.com/hexops/soundio +[submodule "basisu/upstream"] + path = basisu/upstream + url = https://github.com/hexops/basisu [submodule "examples/image-blur/assets"] path = examples/image-blur/assets url = https://github.com/hexops/mach-example-assets diff --git a/basisu/.gitmodules b/basisu/.gitmodules new file mode 100644 index 00000000..abc59fa0 --- /dev/null +++ b/basisu/.gitmodules @@ -0,0 +1,3 @@ +[submodule "upstream"] + path = upstream + url = https://github.com/hexops/basisu \ No newline at end of file diff --git a/basisu/build.zig b/basisu/build.zig new file mode 100644 index 00000000..3d656c86 --- /dev/null +++ b/basisu/build.zig @@ -0,0 +1,124 @@ +const std = @import("std"); +const Builder = std.build.Builder; + +const basisu_root = thisDir() ++ "/upstream/basisu"; + +pub const pkg = std.build.Pkg{ + .name = "basisu", + .source = .{ + .path = "src/main.zig", + }, +}; + +pub fn build(b: *Builder) void { + const mode = b.standardReleaseOptions(); + const target = b.standardTargetOptions(.{}); + + const test_step = b.step("test", "Run library tests"); + test_step.dependOn(&testStep(b, mode, target).step); +} + +pub fn testStep(b: *Builder, mode: std.builtin.Mode, target: std.zig.CrossTarget) *std.build.RunStep { + const main_tests = b.addTestExe("basisu-tests", comptime thisDir() ++ "/src/main.zig"); + main_tests.setBuildMode(mode); + main_tests.setTarget(target); + main_tests.main_pkg_path = thisDir(); + link(b, main_tests, .{ + .encoder = true, + .transcoder = true, + }); + main_tests.install(); + return main_tests.run(); +} + +pub fn link(b: *Builder, step: *std.build.LibExeObjStep, options: Options) void { + if (options.encoder) { + step.linkLibrary(buildEncoder(b)); + step.addCSourceFile(comptime thisDir() ++ "/src/encoder/wrapper.cpp", &.{}); + step.addIncludeDir(basisu_root ++ "/encoder"); + } + if (options.transcoder) { + step.linkLibrary(buildTranscoder(b)); + step.addCSourceFile(comptime thisDir() ++ "/src/transcoder/wrapper.cpp", &.{}); + step.addIncludeDir(basisu_root ++ "/transcoder"); + } +} + +pub fn buildEncoder(b: *Builder) *std.build.LibExeObjStep { + // TODO(build-system): https://github.com/hexops/mach/issues/229#issuecomment-1100958939 + ensureDependencySubmodule(b.allocator, "upstream") catch unreachable; + + const encoder = b.addStaticLibrary("basisu-encoder", null); + encoder.linkLibCpp(); + encoder.addCSourceFiles( + encoder_sources, + &.{}, + ); + + encoder.defineCMacro("BASISU_FORCE_DEVEL_MESSAGES", "0"); + encoder.defineCMacro("BASISD_SUPPORT_KTX2_ZSTD", "0"); + encoder.install(); + return encoder; +} + +pub fn buildTranscoder(b: *Builder) *std.build.LibExeObjStep { + // TODO(build-system): https://github.com/hexops/mach/issues/229#issuecomment-1100958939 + ensureDependencySubmodule(b.allocator, "upstream") catch unreachable; + + const transcoder = b.addStaticLibrary("basisu-transcoder", null); + transcoder.linkLibCpp(); + transcoder.addCSourceFiles( + transcoder_sources, + &.{}, + ); + + transcoder.defineCMacro("BASISU_FORCE_DEVEL_MESSAGES", "0"); + transcoder.defineCMacro("BASISD_SUPPORT_KTX2_ZSTD", "0"); + transcoder.install(); + return transcoder; +} + +pub const Options = struct { + encoder: bool, + transcoder: bool, +}; + +fn thisDir() []const u8 { + return std.fs.path.dirname(@src().file) orelse "."; +} + +fn ensureDependencySubmodule(allocator: std.mem.Allocator, path: []const u8) !void { + if (std.process.getEnvVarOwned(allocator, "NO_ENSURE_SUBMODULES")) |no_ensure_submodules| { + if (std.mem.eql(u8, no_ensure_submodules, "true")) return; + } else |_| {} + var child = std.ChildProcess.init(&.{ "git", "submodule", "update", "--init", path }, allocator); + child.cwd = (comptime thisDir()); + child.stderr = std.io.getStdErr(); + child.stdout = std.io.getStdOut(); + + _ = try child.spawnAndWait(); +} + +const transcoder_sources = &[_][]const u8{ + basisu_root ++ "/transcoder/basisu_transcoder.cpp", +}; + +const encoder_sources = &[_][]const u8{ + basisu_root ++ "/encoder/basisu_backend.cpp", + basisu_root ++ "/encoder/basisu_basis_file.cpp", + basisu_root ++ "/encoder/basisu_bc7enc.cpp", + basisu_root ++ "/encoder/basisu_comp.cpp", + basisu_root ++ "/encoder/basisu_enc.cpp", + basisu_root ++ "/encoder/basisu_etc.cpp", + basisu_root ++ "/encoder/basisu_frontend.cpp", + basisu_root ++ "/encoder/basisu_gpu_texture.cpp", + basisu_root ++ "/encoder/basisu_kernels_sse.cpp", + basisu_root ++ "/encoder/basisu_opencl.cpp", + basisu_root ++ "/encoder/basisu_pvrtc1_4.cpp", + basisu_root ++ "/encoder/basisu_resample_filters.cpp", + basisu_root ++ "/encoder/basisu_resampler.cpp", + basisu_root ++ "/encoder/basisu_ssim.cpp", + basisu_root ++ "/encoder/basisu_uastc_enc.cpp", + basisu_root ++ "/encoder/jpgd.cpp", + basisu_root ++ "/encoder/pvpngreader.cpp", +}; diff --git a/basisu/src/encoder.zig b/basisu/src/encoder.zig new file mode 100644 index 00000000..825f467d --- /dev/null +++ b/basisu/src/encoder.zig @@ -0,0 +1,259 @@ +const std = @import("std"); +const b = @import("encoder/binding.zig"); +const BasisTextureFormat = @import("main.zig").BasisTextureFormat; +const testing = std.testing; + +/// Must be called before encoding anything +pub fn init_encoder() void { + b.basisu_encoder_init(); +} + +pub const Compressor = struct { + pub const Error = error{ + InitializationFailed, + ValidationFailed, + EncodingUASTCFailed, + CannotReadSourceImages, + FrontendFault, + FrontendExtractionFailed, + BackendFault, + CannotCreateBasisFile, + CannotWriteOutput, + UASTCRDOPostProcessFailed, + CannotCreateKTX2File, + }; + + handle: *b.Compressor, + + pub fn init(params: CompressorParams) error{Unknown}!Compressor { + return Compressor{ + .handle = if (b.compressor_init(params.handle)) |v| v else return error.Unknown, + }; + } + + pub fn deinit(self: Compressor) void { + b.compressor_deinit(self.handle); + } + + pub fn process(self: Compressor) Error!void { + return switch (b.compressor_process(self.handle)) { + 0 => {}, + 1 => error.InitializationFailed, + 2 => error.CannotReadSourceImages, + 3 => error.ValidationFailed, + 4 => error.EncodingUASTCFailed, + 5 => error.FrontendFault, + 6 => error.FrontendExtractionFailed, + 7 => error.BackendFault, + 8 => error.CannotCreateBasisFile, + 9 => error.UASTCRDOPostProcessFailed, + 10 => error.CannotCreateKTX2File, + else => unreachable, + }; + } + + /// output will be freed with `Compressor.deinit` + pub fn output(self: Compressor) []const u8 { + return b.compressor_get_output(self.handle)[0..b.compressor_get_output_size(self.handle)]; + } + + pub fn outputBitsPerTexel(self: Compressor) f64 { + return b.compressor_get_output_bits_per_texel(self.handle); + } + + pub fn anyImageHasAlpha(self: Compressor) bool { + return b.compressor_get_any_source_image_has_alpha(self.handle); + } +}; + +pub const CompressorParams = struct { + handle: *b.CompressorParams, + + pub fn init(threads_count: u32) CompressorParams { + const h = CompressorParams{ .handle = b.compressor_params_init() }; + h.setStatusOutput(false); + b.compressor_params_set_thread_count(h.handle, threads_count); + return h; + } + + pub fn deinit(self: CompressorParams) void { + b.compressor_params_deinit(self.handle); + } + + pub fn clear(self: CompressorParams) void { + b.compressor_params_clear(self.handle); + } + + pub fn setStatusOutput(self: CompressorParams, enable_output: bool) void { + b.compressor_params_set_status_output(self.handle, enable_output); + } + + /// `level` ranges from [1, 255] + pub fn setQualityLevel(self: CompressorParams, level: u8) void { + b.compressor_params_set_quality_level(self.handle, level); + } + + pub fn getPackUASTCFlags(self: CompressorParams) PackUASTCFlags { + return PackUASTCFlags.from(b.compressor_params_get_pack_uastc_flags(self.handle)); + } + + pub fn setPackUASTCFlags(self: CompressorParams, flags: PackUASTCFlags) void { + b.compressor_params_set_pack_uastc_flags(self.handle, flags.cast()); + } + + pub fn setBasisFormat(self: CompressorParams, format: BasisTextureFormat) void { + b.compressor_params_set_uastc(self.handle, switch (format) { + .etc1s => false, + .uastc4x4 => true, + }); + } + + pub fn setColorSpace(self: CompressorParams, color_space: ColorSpace) void { + b.compressor_params_set_perceptual(self.handle, switch (color_space) { + .linear => false, + .srgb => true, + }); + } + + pub fn setMipColorSpace(self: CompressorParams, color_space: ColorSpace) void { + b.compressor_params_set_mip_srgb(self.handle, switch (color_space) { + .linear => false, + .srgb => true, + }); + } + + /// Disable selector RDO, for faster compression but larger files. + /// Enabled by default + pub fn setSelectorRDO(self: CompressorParams, enable: bool) void { + b.compressor_params_set_no_selector_rdo(self.handle, !enable); + } + + /// Enabled by default + pub fn setEndpointRDO(self: CompressorParams, enable: bool) void { + b.compressor_params_set_no_endpoint_rdo(self.handle, !enable); + } + + pub fn setRDO_UASTC(self: CompressorParams, enable: bool) void { + b.compressor_params_set_rdo_uastc(self.handle, enable); + } + + pub fn setRDO_UASTCQualityScalar(self: CompressorParams, quality: f32) void { + b.compressor_params_set_rdo_uastc_quality_scalar(self.handle, quality); + } + + pub fn setGenerateMipMaps(self: CompressorParams, enable: bool) void { + b.compressor_params_set_generate_mipmaps(self.handle, enable); + } + + pub fn setMipSmallestDimension(self: CompressorParams, smallest_dimension: i32) void { + b.compressor_params_set_mip_smallest_dimension(self.handle, smallest_dimension); + } + + /// Resizes sources list and creates a new Image in case index is out of bounds + pub fn getImageSource(self: CompressorParams, index: u32) Image { + return .{ .handle = b.compressor_params_get_or_create_source_image(self.handle, index) }; + } + + pub fn resizeImageSource(self: CompressorParams, new_size: u32) void { + b.compressor_params_resize_source_image_list(self.handle, new_size); + } + + pub fn clearImageSource(self: CompressorParams) void { + b.compressor_params_clear_source_image_list(self.handle); + } +}; + +pub const Image = struct { + handle: *b.Image, + + pub fn fill(self: Image, data: []const u8, w: u32, h: u32, channels: u3) void { + b.compressor_image_fill(self.handle, data.ptr, w, h, channels); + } + + pub fn resize(self: Image, w: u32, h: u32, p: ?u32) void { + b.compressor_image_resize(self.handle, w, h, p orelse std.math.maxInt(u32)); + } + + pub fn width(self: Image) u32 { + return b.compressor_image_get_width(self.handle); + } + + pub fn height(self: Image) u32 { + return b.compressor_image_get_height(self.handle); + } + + pub fn pitch(self: Image) u32 { + return b.compressor_image_get_pitch(self.handle); + } + + pub fn totalPixels(self: Image) u32 { + return b.compressor_image_get_total_pixels(self.handle); + } + + pub fn blockWidth(self: Image, w: u32) u32 { + return (self.width() + (w - 1)) / w; + } + + pub fn blockHeight(self: Image, h: u32) u32 { + return (self.height() + (h - 1)) / h; + } + + pub fn totalBlocks(self: Image, w: u32, h: u32) u32 { + return self.blockWidth(w) * self.blockHeight(h); + } +}; + +pub const PackUASTCFlags = packed struct { + fastest: bool = false, + faster: bool = false, + default: bool = false, + slower: bool = false, + verySlow: bool = false, + mask: bool = false, + favor_uastc_error: bool = false, + favor_bc7_error: bool = false, + etc1_faster_hints: bool = false, + etc1_fastest_hints: bool = false, + etc1_disable_flip_and_individual: bool = false, + favor_simpler_modes: bool = false, + + pub const Flag = enum(u32) { + fastest = 0, + faster = 1, + default = 2, + slower = 3, + verySlow = 4, + mask = 0xF, + favor_uastc_error = 8, + favor_bc7_error = 16, + etc1_faster_hints = 64, + etc1_fastest_hints = 128, + etc1_disable_flip_and_individual = 256, + favor_simpler_modes = 512, + }; + + pub fn from(bits: u32) PackUASTCFlags { + var value = PackUASTCFlags{}; + inline for (comptime std.meta.fieldNames(Flag)) |field_name| { + if (bits & (@enumToInt(@field(Flag, field_name))) != 0) { + @field(value, field_name) = true; + } + } + return value; + } + + pub fn cast(self: PackUASTCFlags) u32 { + var value: u32 = 0; + inline for (comptime std.meta.fieldNames(Flag)) |field_name| { + if (@field(self, field_name)) { + value |= @enumToInt(@field(Flag, field_name)); + } + } + return value; + } +}; + +pub const ColorSpace = enum { + linear, + srgb, +}; diff --git a/basisu/src/encoder/binding.zig b/basisu/src/encoder/binding.zig new file mode 100644 index 00000000..aa9e86e6 --- /dev/null +++ b/basisu/src/encoder/binding.zig @@ -0,0 +1,42 @@ +pub const Compressor = opaque {}; +pub const CompressorParams = opaque {}; +pub const Image = opaque {}; + +pub extern fn basisu_encoder_init() void; + +pub extern fn compressor_params_init() *CompressorParams; +pub extern fn compressor_params_deinit(*CompressorParams) void; +pub extern fn compressor_params_clear(*CompressorParams) void; +pub extern fn compressor_params_set_status_output(*CompressorParams, bool) void; +pub extern fn compressor_params_set_thread_count(*CompressorParams, u32) void; +pub extern fn compressor_params_set_quality_level(*CompressorParams, c_int) void; +pub extern fn compressor_params_get_pack_uastc_flags(*CompressorParams) u32; +pub extern fn compressor_params_set_pack_uastc_flags(*CompressorParams, u32) void; +pub extern fn compressor_params_set_uastc(*CompressorParams, bool) void; +pub extern fn compressor_params_set_perceptual(*CompressorParams, bool) void; +pub extern fn compressor_params_set_mip_srgb(*CompressorParams, bool) void; +pub extern fn compressor_params_set_no_selector_rdo(*CompressorParams, bool) void; +pub extern fn compressor_params_set_no_endpoint_rdo(*CompressorParams, bool) void; +pub extern fn compressor_params_set_rdo_uastc(*CompressorParams, bool) void; +pub extern fn compressor_params_set_rdo_uastc_quality_scalar(*CompressorParams, f32) void; +pub extern fn compressor_params_set_generate_mipmaps(*CompressorParams, bool) void; +pub extern fn compressor_params_set_mip_smallest_dimension(*CompressorParams, c_int) void; +pub extern fn compressor_params_get_or_create_source_image(*CompressorParams, u32) *Image; +pub extern fn compressor_params_resize_source_image_list(*CompressorParams, usize) void; +pub extern fn compressor_params_clear_source_image_list(*CompressorParams) void; + +pub extern fn compressor_image_fill(*Image, [*]const u8, u32, u32, u32) void; +pub extern fn compressor_image_resize(*Image, u32, u32, u32) void; +pub extern fn compressor_image_get_width(*Image) u32; +pub extern fn compressor_image_get_height(*Image) u32; +pub extern fn compressor_image_get_pitch(*Image) u32; +pub extern fn compressor_image_get_total_pixels(*Image) u32; + +pub extern fn compressor_init(*CompressorParams) ?*Compressor; +pub extern fn compressor_deinit(*Compressor) void; +pub extern fn compressor_process(*Compressor) u32; + +pub extern fn compressor_get_output(*Compressor) [*]const u8; +pub extern fn compressor_get_output_size(*Compressor) u32; +pub extern fn compressor_get_output_bits_per_texel(*Compressor) f64; +pub extern fn compressor_get_any_source_image_has_alpha(*Compressor) bool; diff --git a/basisu/src/encoder/wrapper.cpp b/basisu/src/encoder/wrapper.cpp new file mode 100644 index 00000000..d3d48f82 --- /dev/null +++ b/basisu/src/encoder/wrapper.cpp @@ -0,0 +1,176 @@ +#include +#include +#include +#include + +extern "C" { +void basisu_encoder_init() { basisu::basisu_encoder_init(); } + +basisu::basis_compressor_params *compressor_params_init() { + return new basisu::basis_compressor_params(); +}; + +void compressor_params_deinit(basisu::basis_compressor_params *params) { + delete params; +} + +void compressor_params_clear(basisu::basis_compressor_params *params) { + params->clear(); +} + +void compressor_params_set_status_output( + basisu::basis_compressor_params *params, bool status_output) { + params->m_status_output = status_output; +} + +void compressor_params_set_thread_count(basisu::basis_compressor_params *params, + uint32_t thread_count) { + params->m_pJob_pool = new basisu::job_pool(thread_count); +} + +void compressor_params_set_quality_level( + basisu::basis_compressor_params *params, uint8_t quality_level) { + params->m_quality_level = quality_level; +} + +uint32_t compressor_params_get_pack_uastc_flags( + basisu::basis_compressor_params *params) { + return params->m_pack_uastc_flags; +} + +void compressor_params_set_pack_uastc_flags( + basisu::basis_compressor_params *params, uint32_t pack_uastc_flags) { + params->m_pack_uastc_flags = pack_uastc_flags; +} + +void compressor_params_set_uastc(basisu::basis_compressor_params *params, + bool is_uastc) { + params->m_uastc = is_uastc; +} + +void compressor_params_set_perceptual(basisu::basis_compressor_params *params, + bool perceptual) { + params->m_perceptual = perceptual; +} + +void compressor_params_set_mip_srgb(basisu::basis_compressor_params *params, + bool mip_srgb) { + params->m_mip_srgb = mip_srgb; +} + +void compressor_params_set_no_selector_rdo( + basisu::basis_compressor_params *params, bool no_selector_rdo) { + params->m_no_selector_rdo = no_selector_rdo; +} + +void compressor_params_set_no_endpoint_rdo( + basisu::basis_compressor_params *params, bool no_endpoint_rdo) { + params->m_no_endpoint_rdo = no_endpoint_rdo; +} + +void compressor_params_set_rdo_uastc(basisu::basis_compressor_params *params, + bool rdo_uastc) { + params->m_rdo_uastc = rdo_uastc; +} + +void compressor_params_set_generate_mipmaps( + basisu::basis_compressor_params *params, bool generate_mipmaps) { + params->m_mip_gen = generate_mipmaps; +} + +void compressor_params_set_rdo_uastc_quality_scalar( + basisu::basis_compressor_params *params, float rdo_uastc_quality_scalar) { + params->m_rdo_uastc_quality_scalar = rdo_uastc_quality_scalar; +} + +void compressor_params_set_mip_smallest_dimension( + basisu::basis_compressor_params *params, int mip_smallest_dimension) { + params->m_mip_smallest_dimension = mip_smallest_dimension; +} + +basisu::image *compressor_params_get_or_create_source_image( + basisu::basis_compressor_params *params, uint32_t index) { + if (params->m_source_images.size() < index + 1) { + params->m_source_images.resize(index + 1); + } + + return ¶ms->m_source_images[index]; +} + +void compressor_params_resize_source_image_list( + basisu::basis_compressor_params *params, size_t size) { + params->m_source_images.resize(size); +} + +void compressor_params_clear_source_image_list( + basisu::basis_compressor_params *params) { + params->clear(); +} + +/// + +void compressor_image_fill(basisu::image *image, const uint8_t *pData, + uint32_t width, uint32_t height, uint32_t comps) { + image->init(pData, width, height, comps); +} + +void compressor_image_resize(basisu::image *image, uint32_t w, uint32_t h, + uint32_t p) { + image->resize(w, h, p); +} + +uint32_t compressor_image_get_width(basisu::image *image) { + return image->get_width(); +} + +uint32_t compressor_image_get_height(basisu::image *image) { + return image->get_height(); +} + +uint32_t compressor_image_get_pitch(basisu::image *image) { + return image->get_pitch(); +} + +uint32_t compressor_image_get_total_pixels(basisu::image *image) { + return image->get_total_pixels(); +} + +/// + +basisu::basis_compressor * +compressor_init(basisu::basis_compressor_params *params) { + auto comp = new basisu::basis_compressor(); + if (comp->init(*params)) + return comp; + else + return nullptr; +} + +void compressor_deinit(basisu::basis_compressor *compressor) { + delete compressor; +} + +basisu::basis_compressor::error_code +compressor_process(basisu::basis_compressor *compressor) { + return compressor->process(); +} + +const uint8_t *compressor_get_output(basisu::basis_compressor *compressor) { + return compressor->get_output_basis_file().data(); +} + +uint32_t +compressor_get_output_size(const basisu::basis_compressor *compressor) { + return compressor->get_basis_file_size(); +} + +double compressor_get_output_bits_per_texel( + const basisu::basis_compressor *compressor) { + return compressor->get_basis_bits_per_texel(); +} + +bool compressor_get_any_source_image_has_alpha( + const basisu::basis_compressor *compressor) { + return compressor->get_any_source_image_has_alpha(); +} +} \ No newline at end of file diff --git a/basisu/src/main.zig b/basisu/src/main.zig new file mode 100644 index 00000000..5a57de91 --- /dev/null +++ b/basisu/src/main.zig @@ -0,0 +1,45 @@ +pub usingnamespace @import("encoder.zig"); +pub usingnamespace @import("transcoder.zig"); + +pub const BasisTextureFormat = enum(u1) { + etc1s = 0, + uastc4x4 = 1, +}; + +// Test + +const t = @import("transcoder.zig"); +const e = @import("encoder.zig"); +const testing = @import("std").testing; + +test "reference decls" { + testing.refAllDeclsRecursive(t); + testing.refAllDeclsRecursive(e); +} + +test "encode/transcode" { + // Encode + e.init_encoder(); + + const params = e.CompressorParams.init(1); + params.setGenerateMipMaps(true); + params.setBasisFormat(.uastc4x4); + params.setPackUASTCFlags(.{ .fastest = true }); + defer params.deinit(); + + const image = params.getImageSource(0); + image.fill(@embedFile("../test/ziggy.png"), 379, 316, 4); + + const comp = try e.Compressor.init(params); + try comp.process(); + + // Transcode + t.init_transcoder(); + + const trans = try t.Transcoder.init(comp.output()); + defer trans.deinit(); + + var out_buf = try testing.allocator.alloc(u8, try trans.calcTranscodedSize(0, 0, .astc_4x4_rgba)); + defer testing.allocator.free(out_buf); + try trans.transcode(out_buf, 0, 0, .astc_4x4_rgba, .{}); +} diff --git a/basisu/src/transcoder.zig b/basisu/src/transcoder.zig new file mode 100644 index 00000000..aaa2f55d --- /dev/null +++ b/basisu/src/transcoder.zig @@ -0,0 +1,229 @@ +const std = @import("std"); +const b = @import("transcoder/binding.zig"); +const BasisTextureFormat = @import("main.zig").BasisTextureFormat; +const testing = std.testing; + +/// Must be called before a `.basis` file can be transcoded. +/// NOTE: this function *isn't* thread safe. +pub fn init_transcoder() void { + b.basisu_transcoder_init(); +} + +/// Returns true if the specified format was enabled at compile time. +pub fn isFormatEnabled(self: BasisTextureFormat, transcoder_format: Transcoder.TextureFormat) bool { + return b.transcoder_is_format_supported(@enumToInt(self), @enumToInt(transcoder_format)); +} + +pub const Transcoder = struct { + handle: *b.BasisFile, + + pub fn init(src: []const u8) error{Unknown}!Transcoder { + const h = b.transcoder_init(src.ptr, @intCast(u32, src.len)); + return if (!b.transcoder_start_transcoding(h)) + error.Unknown + else .{ .handle = h }; + } + + pub fn deinit(self: Transcoder) void { + if (!b.transcoder_stop_transcoding(self.handle)) + unreachable; + b.transcoder_deinit(self.handle); + } + + /// Returns the total number of images in the basis file (always 1 or more). + /// Note that the number of mipmap levels for each image may differ, and that images may have different resolutions. + pub fn getImageCount(self: Transcoder) u32 { + return b.transcoder_get_images_count(self.handle); + } + + /// Returns the number of mipmap levels in an image. + pub fn getImageLevelCount(self: Transcoder, image_index: u32) u32 { + return b.transcoder_get_levels_count(self.handle, image_index); + } + + /// Returns basic information about an image. + /// Note that orig_width/orig_height may not be a multiple of 4. + pub fn getImageLevelDescriptor(self: Transcoder, image_index: u32, level_index: u32) error{OutOfBoundsLevelIndex}!ImageLevelDescriptor { + var desc: ImageLevelDescriptor = undefined; + return if (b.transcoder_get_image_level_desc( + self.handle, + image_index, + level_index, + &desc.original_width, + &desc.original_height, + &desc.block_count, + )) + desc + else + error.OutOfBoundsLevelIndex; + } + + /// Returns the bytes neeeded to store output. + pub fn calcTranscodedSize(self: Transcoder, image_index: u32, level_index: u32, format: TextureFormat) error{OutOfBoundsLevelIndex}!u32 { + var size: u32 = undefined; + return if (b.transcoder_get_image_transcoded_size(self.handle, image_index, level_index, @enumToInt(format), &size)) + size + else + error.OutOfBoundsLevelIndex; + } + + pub const TranscodeParams = struct { + decode_flags: ?DecodeFlags = null, + /// Output row pitch in blocks or pixels. + /// Should be at least the image level's total_blocks (num_blocks_x * num_blocks_y), + /// or the total number of output pixels if fmt==cTFRGBA32. + output_row_pitch: ?u32 = null, + /// Output rows in pixels + /// Ignored unless fmt is uncompressed (cRGBA32, etc.). + /// The total number of output rows in the output buffer. If 0, + /// the transcoder assumes the slice's orig_height (NOT num_blocks_y * 4). + output_rows: ?u32 = null, + }; + + /// Decodes a single mipmap level from the .basis file to any of the supported output texture formats. + /// Currently, to decode to PVRTC1 the basis texture's dimensions in pixels must be a power of 2, + /// due to PVRTC1 format requirements. + /// NOTE: + /// - `transcoder_init()` must have been called first to initialize + /// the transcoder lookup tables before calling this function. + /// - This method assumes the output texture buffer is readable. + /// In some cases to handle alpha, the transcoder will write temporary data + /// to the output texture in a first pass, which will be read in a second pass. + pub fn transcode( + self: Transcoder, + out_buf: []u8, + image_index: u32, + level_index: u32, + format: TextureFormat, + params: TranscodeParams, + ) error{Unknown}!void { + if (!b.transcoder_transcode( + self.handle, + out_buf.ptr, + @intCast(u32, out_buf.len), + image_index, + level_index, + @enumToInt(format), + if (params.decode_flags) |f| f.cast() else 0, + params.output_row_pitch orelse 0, + params.output_rows orelse 0, + )) return error.Unknown; + } + + pub const ImageLevelDescriptor = struct { + original_width: u32, + original_height: u32, + block_count: u32, + }; + + pub const DecodeFlags = packed struct { + pvrtc_decode_to_next_pow_2: bool = false, + transcode_alpha_data_to_opaque_formats: bool = false, + bc1_forbid_three_color_blocks: bool = false, + output_has_alpha_indices: bool = false, + high_quality: bool = false, + + pub const Flag = enum(u32) { + pvrtc_decode_to_next_pow_2 = 2, + transcode_alpha_data_to_opaque_formats = 4, + bc1_forbid_three_color_blocks = 8, + output_has_alpha_indices = 16, + high_quality = 32, + }; + + pub fn from(bits: u32) DecodeFlags { + var value = DecodeFlags{}; + inline for (comptime std.meta.fieldNames(Flag)) |field_name| { + if (bits & (@enumToInt(@field(Flag, field_name))) != 0) { + @field(value, field_name) = true; + } + } + return value; + } + + pub fn cast(self: DecodeFlags) u32 { + var value: u32 = 0; + inline for (comptime std.meta.fieldNames(Flag)) |field_name| { + if (@field(self, field_name)) { + value |= @enumToInt(@field(Flag, field_name)); + } + } + return value; + } + }; + + pub const TextureFormat = enum(u5) { + etc1_rgb = 0, + etc2_rgba = 1, + bc1_rgb = 2, + bc3_rgba = 3, + bc4_r = 4, + bc5_rg = 5, + bc7_rgba = 6, + bc7_alt = 7, + pvrtc1_4_rgb = 8, + pvrtc1_4_rgba = 9, + astc_4x4_rgba = 10, + atc_rgb = 11, + atc_rgba = 12, + rgba32 = 13, + rgb565 = 14, + bgr565 = 15, + rgba4444 = 16, + fxt1_rgb = 17, + pvrtc2_4_rgb = 18, + pvrtc2_4_rgba = 19, + etc2_eac_r11 = 20, + etc2_eac_rg11 = 21, + + pub fn isEnabled( + self: TextureFormat, + basis_texture_format: BasisTextureFormat, + ) bool { + return isFormatEnabled(basis_texture_format, self); + } + + pub fn bytesPerBlockOrPixel(self: TextureFormat) u5 { + return switch (self) { + .rgb565, .bgr565, .rgba4444 => return 2, + .rgba32 => return 4, + .etc1_rgb, .bc1_rgb, .bc4_r, .pvrtc1_4_rgb, .pvrtc1_4_rgba, .atc_rgb, .pvrtc2_4_rgb, .pvrtc2_4_rgba, .etc2_eac_r11 => 8, + .bc7_rgba, .bc7_alt, .etc2_rgba, .bc3_rgba, .bc5_rg, .astc_4x4_rgba, .atc_rgba, .fxt1_rgb, .etc2_eac_rg11 => return 16, + }; + } + }; + + pub const BlockFormat = enum(u5) { + etc1 = 0, + etc2_rgba = 1, + bc1 = 2, + bc3 = 3, + bc4 = 4, + bc5 = 5, + pvrtc1_4_rgb = 6, + pvrtc1_4_rgba = 7, + bc7 = 8, + bc7_m5_color = 9, + bc7_m5_alpha = 10, + etc2_eac_a8 = 11, + astc_4x4 = 12, + atc_rgb = 13, + atc_rgba_interpolated_alpha = 14, + fxt1_rgb = 15, + pvrtc2_4_rgb = 16, + pvrtc2_4_rgba = 17, + etc2_eac_r11 = 18, + etc2_eac_rg11 = 19, + indices = 20, + rgb32 = 21, + rgba32 = 23, + a32 = 24, + rgb565 = 25, + bgr565 = 26, + rgba4444_color = 27, + rgba4444_alpha = 28, + rgba4444_color_opaque = 29, + rgba4444 = 30, + uastc_4x4 = 31, + }; +}; diff --git a/basisu/src/transcoder/binding.zig b/basisu/src/transcoder/binding.zig new file mode 100644 index 00000000..b012e737 --- /dev/null +++ b/basisu/src/transcoder/binding.zig @@ -0,0 +1,38 @@ +pub const BasisFile = opaque {}; + +pub extern fn basisu_transcoder_init() void; + +pub extern fn transcoder_is_format_supported(tex_type: u32, fmt: u32) bool; + +pub extern fn transcoder_init([*]const u8, u32) *BasisFile; +pub extern fn transcoder_deinit(*BasisFile) void; +pub extern fn transcoder_get_images_count(*BasisFile) u32; +pub extern fn transcoder_get_levels_count(*BasisFile, image_index: u32) u32; +pub extern fn transcoder_get_image_level_desc( + *BasisFile, + image_index: u32, + level_index: u32, + orig_width: *u32, + orig_height: *u32, + total_block: *u32, +) bool; +pub extern fn transcoder_get_image_transcoded_size( + *BasisFile, + image_index: u32, + level_index: u32, + format: u32, + size: *u32, +) bool; +pub extern fn transcoder_start_transcoding(*BasisFile) bool; +pub extern fn transcoder_stop_transcoding(*BasisFile) bool; +pub extern fn transcoder_transcode( + *BasisFile, + out: [*]const u8, + out_size: u32, + image_index: u32, + level_index: u32, + format: u32, + decode_flags: u32, + output_row_pitch_in_blocks_or_pixels: u32, + output_rows_in_pixels: u32, +) bool; diff --git a/basisu/src/transcoder/wrapper.cpp b/basisu/src/transcoder/wrapper.cpp new file mode 100644 index 00000000..bad81c98 --- /dev/null +++ b/basisu/src/transcoder/wrapper.cpp @@ -0,0 +1,153 @@ +#include +#include +#include + +extern "C" { +void basisu_transcoder_init(); + +bool transcoder_is_format_supported(uint32_t tex_type, uint32_t fmt); + +void *transcoder_init(void *src, uint32_t src_size); +void transcoder_deinit(void *h); + +uint32_t transcoder_get_images_count(void *h); +uint32_t transcoder_get_levels_count(void *h, uint32_t image_index); + +bool transcoder_get_image_level_desc(void *h, uint32_t image_index, + uint32_t level_index, uint32_t &orig_width, + uint32_t &orig_height, + uint32_t &block_count); + +bool transcoder_get_image_transcoded_size(void *h, uint32_t image_index, + uint32_t level_index, uint32_t format, + uint32_t &size); + +bool transcoder_start_transcoding(void *h); +bool transcoder_stop_transcoding(void *h); + +bool transcoder_transcode(void *h, void *out, uint32_t out_size, + uint32_t image_index, uint32_t level_index, + uint32_t format, uint32_t decode_flags, + uint32_t output_row_pitch_in_blocks_or_pixels, + uint32_t output_rows_in_pixels); +} + +void basisu_transcoder_init() { basist::basisu_transcoder_init(); } + +#define MAGIC 0xDEADBEE1 + +struct basis_file { + int m_magic; + basist::basisu_transcoder m_transcoder; + void *m_pFile; + uint32_t m_file_size; + + basis_file() : m_transcoder() {} +}; + +// transcoder_texture_format, basis_tex_format +bool transcoder_is_format_supported(uint32_t tex_type, uint32_t fmt) { + return basist::basis_is_format_supported( + (basist::transcoder_texture_format)tex_type, + (basist::basis_tex_format)fmt); +} + +// !null - success +// null - validation failure +void *transcoder_init(void *src, uint32_t src_size) { + auto f = new basis_file; + f->m_pFile = src; + f->m_file_size = src_size; + + if (!f->m_transcoder.validate_header(f->m_pFile, f->m_file_size)) { + delete f; + return nullptr; + } + f->m_magic = MAGIC; + + return f; +} + +void transcoder_deinit(void *h) { + auto f = static_cast(h); + delete f; +} + +uint32_t transcoder_get_images_count(void *h) { + auto f = static_cast(h); + return f->m_transcoder.get_total_images(f->m_pFile, f->m_file_size); +} + +uint32_t transcoder_get_levels_count(void *h, uint32_t image_index) { + auto f = static_cast(h); + return f->m_transcoder.get_total_image_levels(f->m_pFile, f->m_file_size, + image_index); +} + +// true - success +// false - OutOfBoundsLevelIndex +bool transcoder_get_image_level_desc(void *h, uint32_t image_index, + uint32_t level_index, uint32_t &orig_width, + uint32_t &orig_height, + uint32_t &block_count) { + auto f = static_cast(h); + return f->m_transcoder.get_image_level_desc( + f->m_pFile, f->m_file_size, image_index, level_index, orig_width, + orig_height, block_count); +} + +// true - success +// false - OutOfBoundsLevelIndex +bool transcoder_get_image_transcoded_size(void *h, uint32_t image_index, + uint32_t level_index, uint32_t format, + uint32_t &size) { + auto f = static_cast(h); + uint32_t orig_width, orig_height, total_blocks; + if (!f->m_transcoder.get_image_level_desc( + f->m_pFile, f->m_file_size, image_index, level_index, orig_width, + orig_height, total_blocks)) + return false; + + uint8_t bytes_per_block_or_pixel = basis_get_bytes_per_block_or_pixel( + (basist::transcoder_texture_format)format); + if (basis_transcoder_format_is_uncompressed( + (basist::transcoder_texture_format)format)) { + size = orig_width * orig_height * bytes_per_block_or_pixel; + } else { + size = total_blocks * bytes_per_block_or_pixel; + } + + return true; +} + +// true - success +// false - unknown +bool transcoder_start_transcoding(void *h) { + auto f = static_cast(h); + return f->m_transcoder.start_transcoding(f->m_pFile, f->m_file_size); +} + +// true - success +// false - unknown +bool transcoder_stop_transcoding(void *h) { + auto f = static_cast(h); + return f->m_transcoder.stop_transcoding(); +} + +// true - success +// false - unknown +bool transcoder_transcode(void *h, void *out, uint32_t out_size, + uint32_t image_index, uint32_t level_index, + uint32_t format, uint32_t decode_flags, + uint32_t output_row_pitch_in_blocks_or_pixels, + uint32_t output_rows_in_pixels) { + auto f = static_cast(h); + uint32_t bytes_per_block = basis_get_bytes_per_block_or_pixel( + (basist::transcoder_texture_format)format); + + return f->m_transcoder.transcode_image_level( + f->m_pFile, f->m_file_size, image_index, level_index, out, + out_size / bytes_per_block, (basist::transcoder_texture_format)format, + decode_flags, output_row_pitch_in_blocks_or_pixels, nullptr, + output_rows_in_pixels); +} diff --git a/basisu/test/ziggy.png b/basisu/test/ziggy.png new file mode 100644 index 0000000000000000000000000000000000000000..5d8e470e60687bf83fc3e6b1d7d79f2005af84b4 GIT binary patch literal 31849 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 V?JIw^mVtqR!PC{xWt~$(696D}qr(6I literal 0 HcmV?d00001 diff --git a/basisu/upstream b/basisu/upstream new file mode 160000 index 00000000..d55a3f9f --- /dev/null +++ b/basisu/upstream @@ -0,0 +1 @@ +Subproject commit d55a3f9f06adf9cc8c31f58e894d947cc6026da5 diff --git a/build.zig b/build.zig index 17301d4b..88e5fb58 100644 --- a/build.zig +++ b/build.zig @@ -6,6 +6,7 @@ const system_sdk = @import("glfw/system_sdk.zig"); const glfw = @import("glfw/build.zig"); const ecs = @import("ecs/build.zig"); const freetype = @import("freetype/build.zig"); +const basisu = @import("basisu/build.zig"); const sysaudio = @import("sysaudio/build.zig"); const sysjs = @import("sysjs/build.zig"); const Pkg = std.build.Pkg; @@ -43,6 +44,7 @@ pub fn build(b: *std.build.Builder) void { test_step.dependOn(&glfw.testStep(b, mode, target).step); test_step.dependOn(&ecs.testStep(b, mode, target).step); test_step.dependOn(&freetype.testStep(b, mode, target).step); + test_step.dependOn(&basisu.testStep(b, mode, target).step); test_step.dependOn(&sysaudio.testStep(b, mode, target, .{}).step); // TODO: we need a way to test wasm stuff // test_mach_step.dependOn(&sysjs.testStep(b, mode, target).step);