Skip to content

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 27 Sep 22:52
· 10 commits to trunk since this release
Immutable release. Only release title and notes can be modified.
v0.6.0
32e692f

Migrating to FormatDesc and ImageRef

Format, color space and alpha mode moved from each Surface to one FormatDesc on the image: Image::desc.

convert takes an ImageRef, the borrowed form of Image. SurfaceRef is the borrowed form of Surface. The surface helpers (tight_data, tile_to_blocks, ...) and validate moved to the borrowed types and take the format as an argument.

split_cubemap returns a cubemap Image. CubemapInput::SeparateFaces is removed: build the cubemap Image yourself.

Rust

 let image = Image {
     surfaces: vec![vec![Surface {
         data, width, height, depth: 1, stride, slice_stride: 0,
-        format: Format::R8G8B8A8_SRGB,
-        color_space: ColorSpace::Srgb,
-        alpha: AlphaMode::Straight,
     }]],
     kind: TextureKind::Texture2D,
+    desc: FormatDesc {
+        format: Format::R8G8B8A8_SRGB,
+        color_space: ColorSpace::Srgb,
+        alpha: AlphaMode::Straight,
+    },
 };
-let output = convert(image, settings)?;
+let output = convert(image.to_ref(), settings)?;

To convert borrowed bytes, build an ImageRef:

let image = ImageRef {
    surfaces: vec![vec![SurfaceRef { data: &pixels, width, height, depth: 1, stride, slice_stride: 0 }]],
    kind: TextureKind::Texture2D,
    desc,
};
let output = convert(image, settings)?;

Metadata and helpers:

-let format = image.surfaces[0][0].format;
+let format = image.desc.format;

-let bytes = surface.tight_data();
+let bytes = surface.as_ref().tight_data(image.desc.format);

Cubemaps:

-let faces: [Surface; 6] = split_cubemap(CubemapInput::Cross(atlas))?;
-let cube = Image { surfaces: faces.map(|f| vec![f]).into(), kind: TextureKind::Cubemap };
+let cube: Image = split_cubemap(CubemapInput::Cross { surface: atlas.as_ref(), desc })?;
+// `CubemapInput::Strip { surface, desc }` changes the same way.

-let faces = split_cubemap(CubemapInput::SeparateFaces(Box::new(faces)))?;
+// Faces in +X, -X, +Y, -Y, +Z, -Z order.
+let cube = Image {
+    surfaces: faces.into_iter().map(|face| vec![face]).collect(),
+    kind: TextureKind::Cubemap,
+    desc,
+};

C

ctt_convert, ctt_split_cubemap_cross and ctt_split_cubemap_strip now borrow their input. The caller keeps ownership and must destroy the input with ctt_image_destroy or ctt_surface_destroy. One image can go to more than one ctt_convert call. ctt_image_push_mip still consumes its surface.

-ctt_surface *s = ctt_surface_create(px, len, w, h, 1, w * 4, 0,
-    CTT_FORMAT_R8G8B8A8_SRGB, CTT_COLOR_SPACE_SRGB, CTT_ALPHA_MODE_STRAIGHT);
-ctt_image *img = ctt_image_create(CTT_TEXTURE_KIND_TEXTURE2D);
+ctt_surface *s = ctt_surface_create(px, len, w, h, 1, w * 4, 0);
+ctt_format_desc desc = {
+    CTT_FORMAT_R8G8B8A8_SRGB, CTT_COLOR_SPACE_SRGB, CTT_ALPHA_MODE_STRAIGHT};
+ctt_image *img = ctt_image_create(CTT_TEXTURE_KIND_TEXTURE2D, desc);

-ctt_status st = ctt_convert(img, &cfg, &out); /* consumes img */
+ctt_status st = ctt_convert(img, &cfg, &out);
+ctt_image_destroy(img); /* not consumed */

-ctt_format f = ctt_image_surface_format(img, layer, mip);
+ctt_format f = ctt_image_desc(img).format;

ctt_image_create returns NULL if desc.format is zero or disagrees with desc.color_space.

The ctt_cubemap_input handle is removed. The split functions borrow the atlas and write one cubemap image:

-ctt_cubemap_input *in = ctt_cubemap_input_cross(atlas); /* consumes atlas */
-ctt_surface *faces[6];
-ctt_status st = ctt_split_cubemap(in, faces);           /* consumes in */
+ctt_image *cube = NULL;
+ctt_status st = ctt_split_cubemap_cross(atlas, desc, &cube);
+ctt_surface_destroy(atlas); /* not consumed */

To replace ctt_cubemap_input_separate_faces, create a CTT_TEXTURE_KIND_CUBEMAP image, add six layers with ctt_image_add_layer, and push one face into each with ctt_image_push_mip.

Migrating to matching formats and color spaces

A format must agree with its color space. With ColorSpace::Srgb, a format that has an sRGB variant must be that variant. With ColorSpace::Linear, a format must not be an sRGB variant. Formats without an sRGB variant, such as R16G16B16A16_SFLOAT, are valid with both.

The rule applies to the FormatDesc of the input image and to the target format of convert. Before, R8G8B8A8_UNORM tagged ColorSpace::Srgb was accepted. Now convert returns an error. FormatExt::with_color_space gives the matching variant.

 desc: FormatDesc {
-    format: Format::R8G8B8A8_UNORM,
+    format: Format::R8G8B8A8_SRGB,
     color_space: ColorSpace::Srgb,
     alpha,
 },

 // sRGB output
-format: Some(TargetFormat::Compressed { format: Format::BC7_UNORM_BLOCK, encoder }),
+format: Some(TargetFormat::Compressed { format: Format::BC7_SRGB_BLOCK, encoder }),

In C, the same rule applies to ctt_format_desc and to the target format in ctt_convert_settings. The CLI is not affected: short format names (-f bc7) resolve to the variant that agrees with the output color space.

BC7F encoder

BC7F, the analytical BC7 encoder from Basis Universal, is supported.

Added

  • BC7F bindings and Rust/CLI support for BC7 compression, with six quality presets. Automatic selection prefers BC7F after bc7enc. Rust: Encoder::Bc7f behind the default encoder-bc7f feature. CLI: --bc7f-opts. See BC7F encoder. @cwfitzgerald
  • Equirectangular panorama to cubemap projection with anisotropic filtering. Rust: CubemapInput::Equirectangular with EquirectangularOrientation and EquirectangularFront. CLI: --cubemap-layout equirectangular, --cubemap-face-size, --equirectangular-front and --equirectangular-mirror. Faces are R32G32B32A32_SFLOAT in linear space.
  • FormatExt::with_color_space returns the variant of a format that agrees with a color space.
  • C API: ctt_format_desc, ctt_image_desc, ctt_split_cubemap_cross and ctt_split_cubemap_strip.

Changed

  • BREAKING: Formats must agree with their color space: ColorSpace::Srgb requires the sRGB variant when the format has one (R8G8B8A8_SRGB, not R8G8B8A8_UNORM), and ColorSpace::Linear rejects sRGB variants. ImageRef::validate checks input surfaces, and convert checks the target format against the output color space. The KTX2 and DDS readers with input color-space overrides produce the matching variant. When the CLI is used with short format names (eg. -f bc7, -f rgba8) it derives the full format from the input/output color space. See Migrating to matching formats and color spaces.
  • With the rayon feature, all mips and layers of an image now encode concurrently instead of one surface at a time, improving worker utilization on images with many small surfaces.
  • With the rayon feature, the pre-compression pipeline (load, swizzle, mipmap, store) also runs concurrently across mips and layers; only the per-layer mip resize chain remains serial.
  • CLI: input images are read and decoded in parallel. --threads now governs input decoding as well as compression.
  • CLI: EXR and HDR input without color-space metadata defaults to linear. Integer images (PNG, JPEG, ...) still default to sRGB. Pass --input-color-space srgb for the previous behavior.
  • SIMD load, store and sRGB kernels use fearless_simd for runtime dispatch.
  • BREAKING: Format metadata moved from Surface to Image, convert takes a borrowed ImageRef, and split_cubemap returns a cubemap Image. In the C API, ctt_convert no longer consumes the image. See Migrating to FormatDesc and ImageRef. @cwfitzgerald

Removed

  • BREAKING: CubemapInput::SeparateFaces. Build a cubemap Image from the six faces directly. @cwfitzgerald
  • BREAKING: Error::CubemapNonUniformFaces and the C status CTT_STATUS_CUBEMAP_NON_UNIFORM_FACES. ImageRef::validate reports faces of different sizes as Error::InvalidImage. @cwfitzgerald
  • BREAKING: C API: ctt_surface_format, ctt_surface_color_space, ctt_surface_alpha, ctt_image_surface_format, ctt_image_surface_color_space, ctt_image_surface_alpha, ctt_split_cubemap and the ctt_cubemap_input_* functions and type. @cwfitzgerald

Fixed

  • All encoders accept the sRGB variants of the block formats they support (for example BC7_SRGB_BLOCK). Before, only the UNORM variants were accepted.
  • ImageRef::validate rejects images whose layers have different sizes at the same mip level. Before, such 2D arrays and cubemaps were accepted. @cwfitzgerald
  • AMD encoder: BC4_SNORM_BLOCK and BC5_SNORM_BLOCK encode SNORM input correctly. Before, SNORM input was converted to UNORM and then read as signed, which corrupted the output. UNORM input is converted by value and is not remapped to [-1, 1].
  • Intel and AMD encoders: BC6H_UFLOAT_BLOCK encodes negative input as 0. Before, negative values (for example the ringing of the Lanczos3 mipmap filter near bright HDR pixels) encoded as the f16 maximum, 65504. @cwfitzgerald
  • AMD encoder: BC6H_SFLOAT_BLOCK keeps the sign of negative input. Before, negative values encoded as the f16 maximum, 65504. @cwfitzgerald