/* Crafter®.Graphics Copyright (C) 2026 Catcrafts® catcrafts.net This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 3.0 as published by the Free Software Foundation; This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ // Image2D — portable 2D image type whose API surface is intentionally // backend-specific via #ifdef. On Vulkan it aliases the existing // ImageVulkan (full VkFormat / usage / layout control). On WebGPU it's // a thin handle around an rgba8unorm GPUTexture; sizes are u16 and the // only update path is from a CompressedTextureAsset. // // The "no shared no-op signatures" principle is deliberate: callers do // the same #ifdef the library does, and write the backend-specific // invocation. The unified type name Image2D is the only thing // portable between the two — that's the whole point. export module Crafter.Graphics:Image2D; #ifndef CRAFTER_GRAPHICS_WINDOW_DOM import :ImageVulkan; export namespace Crafter { // Vulkan target: Image2D is just the existing ImageVulkan. New name, // same shape — keeps existing ImageVulkan callers (e.g. examples/ // Decompression) working without a churn-rename. template using Image2D = ImageVulkan; } #endif // !CRAFTER_GRAPHICS_WINDOW_DOM #ifdef CRAFTER_GRAPHICS_WINDOW_DOM import std; import Crafter.Asset; import :DescriptorHeapWebGPU; import :WebGPU; export namespace Crafter { template class Image2D { public: WebGPUTextureRef handle = 0; std::uint16_t width = 0; std::uint16_t height = 0; void Create(std::uint16_t w, std::uint16_t h) { width = w; height = h; handle = WebGPU::wgpuCreateImage2D(w, h); } // CPU-decompress the .ctex blob (no GPU decompression on WebGPU) // and upload via wgpuWriteImage2D. The intermediate `pixels` vector // lives only for the duration of this call — the underlying // queue.writeTexture in JS makes its own copy. void Update(const CompressedTextureAsset& asset) { if (asset.pixelStride != sizeof(PixelType)) { std::println(std::cerr, "Image2D::Update: pixel stride mismatch (got {}, expected {})", asset.pixelStride, sizeof(PixelType)); std::abort(); } std::vector pixels( static_cast(asset.sizeX) * asset.sizeY); std::array, 1> outputs = { std::as_writable_bytes(std::span(pixels)), }; Compression::DecompressCPU(asset.blob, outputs); WebGPU::wgpuWriteImage2D( handle, pixels.data(), static_cast(pixels.size() * sizeof(PixelType)), asset.sizeX, asset.sizeY); } // Register the texture in a descriptor heap slot so a custom RT // pipeline can bind it via UICustomBinding::SampledTexture. ImageSlot AllocateSlot(DescriptorHeapWebGPU& heap) { DescriptorRange r = heap.AllocateImageSlots(1); heap.imageTable[r.firstElement] = handle; return ImageSlot(&heap, r.firstElement); } void Destroy() { if (handle != 0) { WebGPU::wgpuDestroyTexture(handle); handle = 0; } } }; // 2D texture array — `layers` × (w × h) rgba8unorm. Each layer is // populated independently from a CompressedTextureAsset whose dims // must match the array's (w × h). Layer 0 is sampled at array // index 0 in WGSL; bind through UICustomBindingKind::SampledTextureArray. template class Image2DArray { public: WebGPUTextureRef handle = 0; std::uint16_t width = 0; std::uint16_t height = 0; std::uint16_t layers = 0; std::uint8_t mipLevels = 1; // Create an array with `layerCount` × (w × h) layers, each carrying // `mipLevels` mip levels. Pass mipLevels=1 (default) for a single // base level — matching the original no-mip behaviour. Caller is // responsible for uploading each level via UpdateLayer (which // handles CPU mip-chain generation when mipLevels > 1). void Create(std::uint16_t w, std::uint16_t h, std::uint16_t layerCount, std::uint8_t mipLevelCount = 1) { width = w; height = h; layers = layerCount; mipLevels = mipLevelCount; handle = WebGPU::wgpuCreateImage2DArray(w, h, layerCount, mipLevelCount); } // Decompress `tex`, generate a CPU box-filter mip chain (if // mipLevels > 1), and upload each level into `layer`. The asset's // base-level dims must match the array's (w × h) — resize // beforehand on the host with TextureAsset::Resize() if // they don't. Pixel data is treated as raw bytes per channel for // the box filter — for non-color data (normal maps) this gives // approximate but adequate results; for sRGB-encoded color data // it's also approximate but visually fine for game textures. void UpdateLayer(std::uint16_t layer, const CompressedTextureAsset& tex) { if (tex.pixelStride != sizeof(PixelType)) { std::println(std::cerr, "Image2DArray::UpdateLayer: pixel stride mismatch (got {}, expected {})", tex.pixelStride, sizeof(PixelType)); std::abort(); } if (tex.sizeX != width || tex.sizeY != height) { std::println(std::cerr, "Image2DArray::UpdateLayer: layer {} dims {}x{} don't match array dims {}x{}", layer, tex.sizeX, tex.sizeY, width, height); std::abort(); } std::vector pixels(static_cast(width) * height); std::array, 1> outputs = { std::as_writable_bytes(std::span(pixels)), }; Compression::DecompressCPU(tex.blob, outputs); // Upload level 0. WebGPU::wgpuWriteImage2DLayer( handle, layer, /*level*/ 0, pixels.data(), static_cast(pixels.size() * sizeof(PixelType)), width, height); // Generate + upload subsequent mip levels via a 2x2 box filter // on the previous level's bytes. Each channel is averaged // independently across 4 source texels. std::uint16_t srcW = width; std::uint16_t srcH = height; std::vector prev = std::move(pixels); for (std::uint8_t lvl = 1; lvl < mipLevels; ++lvl) { std::uint16_t dstW = std::max(1, srcW >> 1); std::uint16_t dstH = std::max(1, srcH >> 1); std::vector next(static_cast(dstW) * dstH); constexpr std::size_t kChannels = sizeof(PixelType); auto srcBytes = reinterpret_cast(prev.data()); auto dstBytes = reinterpret_cast(next.data()); for (std::uint16_t y = 0; y < dstH; ++y) { std::uint16_t sy0 = static_cast(y * 2); std::uint16_t sy1 = static_cast(std::min(sy0 + 1, srcH - 1)); for (std::uint16_t x = 0; x < dstW; ++x) { std::uint16_t sx0 = static_cast(x * 2); std::uint16_t sx1 = static_cast(std::min(sx0 + 1, srcW - 1)); std::size_t a = (static_cast(sy0) * srcW + sx0) * kChannels; std::size_t b = (static_cast(sy0) * srcW + sx1) * kChannels; std::size_t c = (static_cast(sy1) * srcW + sx0) * kChannels; std::size_t d = (static_cast(sy1) * srcW + sx1) * kChannels; std::size_t out = (static_cast(y) * dstW + x) * kChannels; for (std::size_t ch = 0; ch < kChannels; ++ch) { std::uint32_t sum = static_cast(srcBytes[a + ch]) + static_cast(srcBytes[b + ch]) + static_cast(srcBytes[c + ch]) + static_cast(srcBytes[d + ch]); dstBytes[out + ch] = static_cast((sum + 2u) >> 2); } } } WebGPU::wgpuWriteImage2DLayer( handle, layer, /*level*/ lvl, next.data(), static_cast(next.size() * sizeof(PixelType)), dstW, dstH); prev = std::move(next); srcW = dstW; srcH = dstH; } } ImageSlot AllocateSlot(DescriptorHeapWebGPU& heap) { DescriptorRange r = heap.AllocateImageSlots(1); heap.imageTable[r.firstElement] = handle; return ImageSlot(&heap, r.firstElement); } void Destroy() { if (handle != 0) { WebGPU::wgpuDestroyTexture(handle); handle = 0; } } }; } #endif // CRAFTER_GRAPHICS_WINDOW_DOM