docs

Compressed Textures

Load block-compressed DDS images into pages to reduce GPU memory and improve load times.

On this page

You can load block-compressed DDS images into pages to reduce video memory and load assets faster than PNG files.

Because the GPU samples compressed blocks directly, textures stay compressed in video memory— using four to eight times less memory than uncompressed surfaces.

AppCore's stock drivers support compressed textures out of the box (custom drivers can add support by handling compressed bitmaps).

📘 Pro Edition Required

Compressed textures require the Pro edition or higher. You can check for support at compile time with UL_HAS(COMPRESSED_TEXTURES) (see Editions and Feature Macros). Below the Pro edition, .dds images don't load.

Using DDS Images on the Page

Reference .dds files anywhere an image URL is accepted, such as an <img> element or a CSS background-image property:

HTML
<img src="hud.dds">

The library recognizes DDS images by the .dds file extension or by the image/vnd.ms-dds and image/x-dds MIME types.

Authoring DDS Files

Converting Images with texconv

Convert source images to DDS using Microsoft's texconv utility:

Shell
texconv -f BC7_UNORM -dx10 -m 1 -pmalpha -o assets hud.png

Pass BC1_UNORM, BC2_UNORM, BC3_UNORM, or BC7_UNORM to -f (the library also accepts their _SRGB counterparts). BC7 requires -dx10 to write a DirectX 10 header— formats from BC1 through BC3 also work with older DXT1, DXT3, or DXT5 headers.

Ultralight uses only the top mip level, so pass -m 1 to keep the file small. The renderer supports 2D textures only (cubemaps and texture arrays are not supported).

🚧 Premultiply Alpha When Converting

The renderer treats all compressed texels as premultiplied alpha. Straight alpha causes translucent edges to blend incorrectly, so pass -pmalpha when converting assets with transparency.

Choosing a Format

Choose a compression format based on the image's alpha requirements and quality needs:

Format Also Known As Block Size Use
BitmapFormat::BC1_UNORM DXT1 8 bytes Opaque images (optional 1-bit alpha)— smallest file size
BitmapFormat::BC2_UNORM DXT3 16 bytes Explicit 4-bit alpha
BitmapFormat::BC3_UNORM DXT5 16 bytes Smooth alpha gradients and fast CPU decode
BitmapFormat::BC7_UNORM None 16 bytes Highest quality color and alpha (slowest CPU decode)

Fallback Without GPU Support

If the GPU driver doesn't advertise support for a format family, or if a View runs on the CPU renderer, the library decompresses the blocks to BGRA in software.

The image renders identically on screen, but without GPU memory savings. Decoding BC1 and BC3 on the CPU is faster than decoding PNG files, while BC7 decodes at roughly the same speed as PNG.

Loading Compressed Blocks from Memory

If you already hold compressed blocks in memory, wrap them in a Bitmap and register an ImageSource (see Displaying Custom Textures):

C++
#include <Ultralight/Ultralight.h>

using namespace ultralight;

void RegisterHUD(const void* blocks, uint32_t width, uint32_t height) {
  ///
  /// Copy BC7 blocks we hold in memory (free them afterward if you like).
  /// Rows are measured in blocks, so size them with the format's block
  /// geometry rather than width * bpp.
  ///
  BitmapFormatInfo info = GetBitmapFormatInfo(BitmapFormat::BC7_UNORM);
  uint32_t row_pitch = ((width + info.block_width - 1) / info.block_width) *
                       info.block_bytes;
  uint32_t block_rows = (height + info.block_height - 1) / info.block_height;

  RefPtr<Bitmap> bitmap = Bitmap::Create(width, height, BitmapFormat::BC7_UNORM,
                                         row_pitch, blocks,
                                         row_pitch * block_rows);

  ///
  /// Register it under the name a .imgsrc file on the page refers to.
  ///
  ImageSourceProvider::instance().AddImageSource("hud",
      ImageSource::CreateFromBitmap(bitmap));
}

Block-compressed formats store 4x4 pixel blocks, so row pitch and buffer sizes are calculated from block dimensions rather than individual pixels (bpp() returns 0 on compressed bitmaps). Call GetBitmapFormatInfo() to inspect block width, height, and byte size.

Compressed bitmaps don't support per-pixel operations. Calls to Bitmap::Erase(), Bitmap::DrawBitmap(), Bitmap::Resample(), and format conversions fail or do nothing on compressed surfaces (see Working with Bitmaps). You can test whether an instance is compressed using Bitmap::is_compressed().

Supporting Compressed Textures in a Custom Driver

Advertising Format Support

Advertise supported format families by setting bits on GPUDeviceCaps::compressed_formats in GPUDriver::GetDeviceCaps() (see GPU Device Capabilities). Custom drivers advertise no formats by default— the library falls back to decompressing blocks on the CPU.

Creating the Native Texture

In GPUDriver::CreateTexture(), check Bitmap::is_compressed() to branch on compressed bitmaps:

C++
#include <Ultralight/Ultralight.h>

using namespace ultralight;

class MyGPUDriver : public GPUDriver {
 public:
  void CreateTexture(uint32_t texture_id, RefPtr<Bitmap> bitmap,
                     uint32_t flags) override {
    if (bitmap->is_compressed()) {
      ///
      /// Lock it read-only and hand the blocks to the GPU as-is. bpp() is 0
      /// here, so size the upload from size() and row_bytes(), never from
      /// width * bpp.
      ///
      auto pixels = bitmap->LockPixelsSafe();
      UploadCompressedBlocks(texture_id, bitmap->format(), bitmap->width(),
                             bitmap->height(), pixels.data(), pixels.size(),
                             bitmap->row_bytes());
      return;
    }

    // Uncompressed formats and render targets: see Implementing a GPUDriver.
  }

 private:
  // Pseudo-code, create a native texture in the block-compressed format
  // matching format and upload num_bytes from blocks with row_pitch bytes per
  // row of blocks. The texture is immutable, so static storage is fine.
  void UploadCompressedBlocks(uint32_t texture_id, BitmapFormat format,
                              uint32_t width, uint32_t height,
                              const void* blocks, size_t num_bytes,
                              uint32_t row_pitch) {}
};

Compressed textures are uploaded once and never updated— flags includes kGPUTextureFlag_Immutable, so you can safely allocate them in static GPU memory.

Selecting Native Formats

The stock reference drivers support compressed formats across several graphics APIs:

Graphics API Reference Driver Support
Direct3D 11, Direct3D 12 All BC formats
Metal When the device supports BC formats
OpenGL BC1 through BC3 with the S3TC extension, BC7 with BPTC

🚧 Use Plain UNORM Formats

Create native textures with plain UNORM formats (eg, DXGI_FORMAT_BC7_UNORM or GL_COMPRESSED_RGBA_BPTC_UNORM), never sRGB formats. The library blends stored sRGB values directly— an sRGB format causes the GPU to decode on sampling, leaving colors too dark.

Inspect the reference driver source in the SDK's platform/ folder for API-specific upload calls.