Crafter.Graphics/interfaces/Crafter.Graphics-Image2D.cppm

224 lines
10 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;
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<RGBA8>::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<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);
// Upload level 0.
WebGPU::wgpuWriteImage2DLayer(
handle, layer, /*level*/ 0,
pixels.data(),
static_cast<std::int32_t>(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<PixelType> prev = std::move(pixels);
for (std::uint8_t lvl = 1; lvl < mipLevels; ++lvl) {
std::uint16_t dstW = std::max<std::uint16_t>(1, srcW >> 1);
std::uint16_t dstH = std::max<std::uint16_t>(1, srcH >> 1);
std::vector<PixelType> next(static_cast<std::size_t>(dstW) * dstH);
constexpr std::size_t kChannels = sizeof(PixelType);
auto srcBytes = reinterpret_cast<const std::uint8_t*>(prev.data());
auto dstBytes = reinterpret_cast<std::uint8_t*>(next.data());
for (std::uint16_t y = 0; y < dstH; ++y) {
std::uint16_t sy0 = static_cast<std::uint16_t>(y * 2);
std::uint16_t sy1 = static_cast<std::uint16_t>(std::min<std::int32_t>(sy0 + 1, srcH - 1));
for (std::uint16_t x = 0; x < dstW; ++x) {
std::uint16_t sx0 = static_cast<std::uint16_t>(x * 2);
std::uint16_t sx1 = static_cast<std::uint16_t>(std::min<std::int32_t>(sx0 + 1, srcW - 1));
std::size_t a = (static_cast<std::size_t>(sy0) * srcW + sx0) * kChannels;
std::size_t b = (static_cast<std::size_t>(sy0) * srcW + sx1) * kChannels;
std::size_t c = (static_cast<std::size_t>(sy1) * srcW + sx0) * kChannels;
std::size_t d = (static_cast<std::size_t>(sy1) * srcW + sx1) * kChannels;
std::size_t out = (static_cast<std::size_t>(y) * dstW + x) * kChannels;
for (std::size_t ch = 0; ch < kChannels; ++ch) {
std::uint32_t sum = static_cast<std::uint32_t>(srcBytes[a + ch])
+ static_cast<std::uint32_t>(srcBytes[b + ch])
+ static_cast<std::uint32_t>(srcBytes[c + ch])
+ static_cast<std::uint32_t>(srcBytes[d + ch]);
dstBytes[out + ch] = static_cast<std::uint8_t>((sum + 2u) >> 2);
}
}
}
WebGPU::wgpuWriteImage2DLayer(
handle, layer, /*level*/ lvl,
next.data(),
static_cast<std::int32_t>(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