2026-05-19 00:27:09 +02:00
|
|
|
|
/*
|
|
|
|
|
|
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;
|
2026-05-24 13:32:08 +02:00
|
|
|
|
std::uint8_t mipLevels = 1;
|
2026-05-19 00:27:09 +02:00
|
|
|
|
|
2026-05-24 13:32:08 +02:00
|
|
|
|
// 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);
|
2026-05-19 00:27:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-24 13:32:08 +02:00
|
|
|
|
// 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.
|
2026-05-19 00:27:09 +02:00
|
|
|
|
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);
|
2026-05-24 13:32:08 +02:00
|
|
|
|
|
|
|
|
|
|
// Upload level 0.
|
2026-05-19 00:27:09 +02:00
|
|
|
|
WebGPU::wgpuWriteImage2DLayer(
|
2026-05-24 13:32:08 +02:00
|
|
|
|
handle, layer, /*level*/ 0,
|
2026-05-19 00:27:09 +02:00
|
|
|
|
pixels.data(),
|
|
|
|
|
|
static_cast<std::int32_t>(pixels.size() * sizeof(PixelType)),
|
|
|
|
|
|
width, height);
|
2026-05-24 13:32:08 +02:00
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
2026-05-19 00:27:09 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|