Files
EVEngine/test/terrain_performance.cpp

170 lines
7.9 KiB
C++

#include "procgen/heightmap/TerrainAsset.h"
#include "procgen/heightmap/TerrainStreaming.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <memory>
using namespace eve;
using namespace eve::procgen;
namespace {
constexpr std::size_t kHeaderSize = 44, kEntrySize = 36;
void put16(std::vector<std::uint8_t>& out, std::uint16_t value) {
out.push_back(std::uint8_t(value));
out.push_back(std::uint8_t(value >> 8));
}
void put32(std::vector<std::uint8_t>& out, std::uint32_t value) {
for (int shift = 0; shift < 32; shift += 8) out.push_back(std::uint8_t(value >> shift));
}
void put64(std::vector<std::uint8_t>& out, std::uint64_t value) {
for (int shift = 0; shift < 64; shift += 8) out.push_back(std::uint8_t(value >> shift));
}
void putFloat(std::vector<std::uint8_t>& out, float value) {
std::uint32_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
put32(out, bits);
}
std::uint32_t checksum(const std::vector<std::uint8_t>& bytes) {
std::uint32_t value = 2166136261U;
for (auto byte : bytes) {
value ^= byte;
value *= 16777619U;
}
return value;
}
double milliseconds(std::chrono::steady_clock::duration duration) {
return std::chrono::duration<double, std::milli>(duration).count();
}
/** Full EVTR address space backed only for metadata and the chunks actually read. */
class SparseTerrainArchiveSource final : public ITerrainArchiveSource {
public:
explicit SparseTerrainArchiveSource(int dimension) {
constexpr int chunkSize = 64;
for (int index = 0; index < chunkSize * chunkSize; ++index) {
put16(chunk_, std::uint16_t(index));
chunk_.insert(chunk_.end(), {255, 0, 128, 128, 0, 0, 128, 128, 0, 2});
}
const int columns = (dimension + chunkSize - 1) / chunkSize;
const auto count = std::uint32_t(columns * columns);
payloadStart_ = kHeaderSize + std::uint64_t(count) * kEntrySize;
metadata_.insert(metadata_.end(), {'E', 'V', 'T', 'R'});
put16(metadata_, 5);
put16(metadata_, 0);
put32(metadata_, dimension);
put32(metadata_, dimension);
put32(metadata_, chunkSize);
put32(metadata_, count);
putFloat(metadata_, 0);
putFloat(metadata_, 1);
putFloat(metadata_, 1);
put64(metadata_, kHeaderSize);
const auto chunkChecksum = checksum(chunk_);
const auto chunkBytes = static_cast<std::uint32_t>(chunk_.size());
std::uint64_t offset = payloadStart_;
for (int y = 0; y < columns; ++y)
for (int x = 0; x < columns; ++x) {
put32(metadata_, x);
put32(metadata_, y);
put16(metadata_, chunkSize);
put16(metadata_, chunkSize);
metadata_.insert(metadata_.end(), {0, 0, 0, 0});
put64(metadata_, offset);
put32(metadata_, chunkBytes);
put32(metadata_, chunkBytes);
put32(metadata_, chunkChecksum);
offset += chunk_.size();
}
size_ = offset;
}
[[nodiscard]] std::uint64_t size() const noexcept override { return size_; }
[[nodiscard]] Result<std::vector<std::uint8_t>> read(std::uint64_t offset, std::size_t length) const override {
bytesRead_ += length;
++reads_;
if (offset <= metadata_.size() && length <= metadata_.size() - offset) {
auto begin = metadata_.begin() + std::ptrdiff_t(offset);
return Result<std::vector<std::uint8_t>>::success({begin, begin + std::ptrdiff_t(length)});
}
if (offset >= payloadStart_ && length == chunk_.size() && (offset - payloadStart_) % chunk_.size() == 0 &&
offset + length <= size_)
return Result<std::vector<std::uint8_t>>::success(chunk_);
return Result<std::vector<std::uint8_t>>::failure(Diagnostic::error(DiagnosticCode::InvalidArgument,
"sparse terrain read is out of range", {},
{}, "procgen.terrain-performance"));
}
[[nodiscard]] std::uint64_t bytesRead() const noexcept { return bytesRead_; }
[[nodiscard]] std::uint64_t reads() const noexcept { return reads_; }
private:
std::uint64_t payloadStart_ = 0, size_ = 0;
std::vector<std::uint8_t> metadata_, chunk_;
mutable std::uint64_t bytesRead_ = 0, reads_ = 0;
};
} // namespace
TEST_CASE("procgen.terrain.performance.real1kRoundTripAnd1k4k8kRandomAccess") {
constexpr int dimension = 1024, chunkSize = 64;
const std::size_t count = std::size_t(dimension) * dimension;
Heightmap heightmap(dimension, dimension);
HydrologyMap hydrology;
hydrology.width = hydrology.height = dimension;
hydrology.flowAccumulation.assign(count, 1);
hydrology.rivers.assign(count, 0);
ClimateMap climate;
climate.width = climate.height = dimension;
climate.temperature.assign(count, .5F);
climate.moisture.assign(count, .5F);
climate.biomes.assign(count, Biome::Grassland);
for (int y = 0; y < dimension; ++y)
for (int x = 0; x < dimension; ++x)
heightmap.setHeight(x, y, .5F + .25F * std::sin(float(x) * .011F) * std::cos(float(y) * .013F));
std::vector<std::uint8_t> archive;
std::string error;
const auto bakeBegin = std::chrono::steady_clock::now();
REQUIRE(TerrainAsset::bake(heightmap, hydrology, climate, chunkSize, archive, &error));
const double bakeMs = milliseconds(std::chrono::steady_clock::now() - bakeBegin);
TerrainAsset roundTrip;
TerrainChunkData decoded;
REQUIRE(roundTrip.open(archive.data(), archive.size(), &error));
REQUIRE(roundTrip.loadChunk(7, 9, decoded, &error));
CHECK(std::abs(decoded.heights.height(3, 5) - heightmap.height(451, 581)) < .001F);
CHECK(bakeMs < 20000);
std::printf("terrain.performance dense_dimension=1024 archive_bytes=%zu bake_ms=%.3f\n", archive.size(), bakeMs);
for (int worldDimension : std::array{1024, 4096, 8192}) {
auto source = std::make_shared<SparseTerrainArchiveSource>(worldDimension);
TerrainStreamingCache streaming;
const auto openBegin = std::chrono::steady_clock::now();
REQUIRE(streaming.openSource(source).ok());
const double openMs = milliseconds(std::chrono::steady_clock::now() - openBegin);
std::vector<double> moves;
for (int index = 0; index < 20; ++index) {
const int coordinate = (index * 389) % (worldDimension - 1);
const auto begin = std::chrono::steady_clock::now();
auto stats = streaming.streamAround(coordinate, worldDimension - 1 - coordinate, 2, 4, &error);
moves.push_back(milliseconds(std::chrono::steady_clock::now() - begin));
CHECK_EQ(stats.failed, 0);
CHECK(stats.resident <= 13);
}
std::sort(moves.begin(), moves.end());
const auto columns = std::uint64_t((worldDimension + chunkSize - 1) / chunkSize);
const auto directoryBytes = kHeaderSize + columns * columns * kEntrySize;
CHECK(source->bytesRead() <= directoryBytes * 2 + 80 * chunkSize * chunkSize * 12);
CHECK(openMs < 1000);
CHECK(moves[18] < 100);
std::printf(
"terrain.performance sparse_dimension=%d virtual_bytes=%llu reads=%llu "
"bytes_read=%llu open_ms=%.3f stream_p95_ms=%.3f\n",
worldDimension, static_cast<unsigned long long>(source->size()),
static_cast<unsigned long long>(source->reads()), static_cast<unsigned long long>(source->bytesRead()),
openMs, moves[18]);
}
}