From 647ad7f254232f59ccba519159b37af4b32de854 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sun, 13 Sep 2026 20:17:24 +0200 Subject: [PATCH] test: cover luminance and colourDistance --- tests/testTileTexture.cpp | 151 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 tests/testTileTexture.cpp diff --git a/tests/testTileTexture.cpp b/tests/testTileTexture.cpp new file mode 100644 index 0000000..fcba8f7 --- /dev/null +++ b/tests/testTileTexture.cpp @@ -0,0 +1,151 @@ +#include +#include +#include + +#include "testHarness.h" +#include "vf/Block.h" +#include "vf/TileTexture.h" + +using namespace vf; + +namespace { + +int luminance(TilePixel texel) { + return (texel.r * 299 + texel.g * 587 + texel.b * 114) / 1000; +} + +int colourDistance(TilePixel texel, TileRgb reference) { + int dr = static_cast(texel.r) - reference.r; + int dg = static_cast(texel.g) - reference.g; + int db = static_cast(texel.b) - reference.b; + return dr * dr + dg * dg + db * db; +} + +bool leansToAccent(TilePixel texel, const TileSpec& spec) { + return colourDistance(texel, spec.accent) < colourDistance(texel, spec.base); +} + +// The palette owns how many tiles exist. A table short of a row must surface as +// a reported failure, not as a read past the end of the table proving it. +int renderedTiles(const TileSampler& sampler) { + int held = sampler.palette().size(); + return held < tileCount ? held : tileCount; +} + +std::vector renderTile(const TileSampler& sampler, int tile) { + int size = sampler.params().tileSize; + std::vector bytes(static_cast(size) * size * 4); + sampler.writeTile(tile, bytes.data()); + return bytes; +} + +std::vector renderAtlas(const TileSampler& sampler, bool backwards) { + int size = sampler.params().tileSize; + std::size_t tileBytes = static_cast(size) * size * 4; + int held = renderedTiles(sampler); + std::vector bytes(tileBytes * static_cast(held)); + if (backwards) { + for (int tile = held - 1; tile >= 0; --tile) { + sampler.writeTile(tile, bytes.data() + tileBytes * static_cast(tile)); + } + } else { + for (int tile = 0; tile < held; ++tile) { + sampler.writeTile(tile, bytes.data() + tileBytes * static_cast(tile)); + } + } + return bytes; +} + +// The first row and column carry a deliberate edge darkening, which would hand +// every tile a luminance spread for free. Contrast is therefore only ever +// measured inside that border. +struct LuminanceRange { + int lowest = 255; + int highest = 0; + int spread() const { return highest - lowest; } +}; + +LuminanceRange interiorRange(const TileSampler& sampler, int tile) { + LuminanceRange range; + int size = sampler.params().tileSize; + for (int y = 1; y < size; ++y) { + for (int x = 1; x < size; ++x) { + int value = luminance(sampler.pixelAt(tile, x, y)); + if (value < range.lowest) range.lowest = value; + if (value > range.highest) range.highest = value; + } + } + return range; +} + +TileSpec withoutGrain(TileSpec spec) { + spec.noise = 0.0f; + return spec; +} + +// Columns in one row that the pattern painted with the accent colour. For a +// brick tile those are the vertical mortar joints. +std::vector accentColumns(const TileSampler& sampler, const TileSpec& spec, int tileSalt, + int y) { + std::vector columns; + for (int x = 0; x < sampler.params().tileSize; ++x) { + if (leansToAccent(sampler.pixelOf(spec, tileSalt, x, y), spec)) columns.push_back(x); + } + return columns; +} + +// A brick course is offset from the one above it, so the set of mortar columns +// has to differ between courses. Rows that are entirely mortar are the +// horizontal joints and say nothing about the offset. +int distinctJointLayouts(const TileSampler& sampler, const TileSpec& spec, int tileSalt) { + int size = sampler.params().tileSize; + std::set> layouts; + for (int y = 0; y < size; ++y) { + std::vector columns = accentColumns(sampler, spec, tileSalt, y); + if (columns.empty()) continue; + if (static_cast(columns.size()) > size / 2) continue; + layouts.insert(columns); + } + return static_cast(layouts.size()); +} + +TileSpec syntheticBrick() { + TileSpec spec; + spec.name = "syntheticBrick"; + spec.pattern = TilePattern::Brick; + spec.material = TileMaterial::Stone; + spec.base = TileRgb{30, 30, 30}; + spec.accent = TileRgb{240, 240, 240}; + spec.noise = 0.0f; + spec.scale = 0.40f; + spec.density = 0.50f; + return spec; +} + +} // namespace + +// Same params in, same bytes out. Anything that reached for a clock, a global +// counter or an uninitialised buffer would show up here first. +VF_TEST(renderingATileTwiceProducesIdenticalBytes) { + TileSampler first; + TileSampler second; + CHECK_EQ(first.palette().size(), tileCount); + for (int tile = 0; tile < renderedTiles(first); ++tile) { + std::vector a = renderTile(first, tile); + std::vector b = renderTile(second, tile); + bool identical = a == b; + CHECK(identical); + if (!identical) std::printf(" tile %d is not reproducible\n", tile); + } +} + +// A tile must not care which tiles were generated before it. The atlas built +// back to front has to match the atlas built front to back byte for byte. +VF_TEST(theAtlasDoesNotDependOnTheOrderTilesAreGenerated) { + TileSampler sampler; + std::vector forwards = renderAtlas(sampler, false); + std::vector backwards = renderAtlas(sampler, true); + CHECK_EQ(forwards.size(), backwards.size()); + CHECK(forwards == backwards); +} +