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 00000000..5d8e470e Binary files /dev/null and b/basisu/test/ziggy.png differ 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);