/* 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; void Create(std::uint16_t w, std::uint16_t h, std::uint16_t layerCount) { width = w; height = h; layers = layerCount; handle = WebGPU::wgpuCreateImage2DArray(w, h, layerCount); } // Decompress `tex` and upload to `layer`. The asset's dims must // match the array's (w × h) — resize beforehand on the host with // TextureAsset::Resize() if they don't. 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); WebGPU::wgpuWriteImage2DLayer( handle, layer, pixels.data(), static_cast(pixels.size() * sizeof(PixelType)), width, height); } 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