Crafter.Graphics/interfaces/Crafter.Graphics-Image2D.cppm
2026-05-19 00:27:09 +02:00

166 lines
6.4 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
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<T> — portable 2D image type whose API surface is intentionally
// backend-specific via #ifdef. On Vulkan it aliases the existing
// ImageVulkan<T> (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<T> 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 <typename PixelType>
using Image2D = ImageVulkan<PixelType>;
}
#endif // !CRAFTER_GRAPHICS_WINDOW_DOM
#ifdef CRAFTER_GRAPHICS_WINDOW_DOM
import std;
import Crafter.Asset;
import :DescriptorHeapWebGPU;
import :WebGPU;
export namespace Crafter {
template <typename PixelType>
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<PixelType> pixels(
static_cast<std::size_t>(asset.sizeX) * asset.sizeY);
std::array<std::span<std::byte>, 1> outputs = {
std::as_writable_bytes(std::span(pixels)),
};
Compression::DecompressCPU(asset.blob, outputs);
WebGPU::wgpuWriteImage2D(
handle,
pixels.data(),
static_cast<std::int32_t>(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 <typename PixelType>
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<RGBA8>::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<PixelType> pixels(static_cast<std::size_t>(width) * height);
std::array<std::span<std::byte>, 1> outputs = {
std::as_writable_bytes(std::span(pixels)),
};
Compression::DecompressCPU(tex.blob, outputs);
WebGPU::wgpuWriteImage2DLayer(
handle, layer,
pixels.data(),
static_cast<std::int32_t>(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