Skip to content

Commit

Permalink
Add C++ bitpacked class
Browse files Browse the repository at this point in the history
  • Loading branch information
HexDecimal committed Nov 9, 2024
1 parent 16e4cb3 commit d80f6f8
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 44 deletions.
1 change: 1 addition & 0 deletions .vscode/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@
"stbds",
"stbtt",
"stdbool",
"STDEXT",
"stdint",
"stepx",
"stepy",
Expand Down
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@
{
"name": "Documentation: Launch Chrome",
"request": "launch",
"type": "pwa-chrome",
"type": "chrome",
"url": "file://${workspaceFolder}/docs/_build/html/index.html",
"webRoot": "${workspaceFolder}",
"preLaunchTask": "build documentation",
},
{
"name": "Documentation: Launch Edge",
"request": "launch",
"type": "pwa-msedge",
"type": "msedge",
"url": "file://${workspaceFolder}/docs/_build/html/index.html",
"webRoot": "${workspaceFolder}",
"preLaunchTask": "build documentation",
Expand Down
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@
"pointers": "cpp",
"stacktrace": "cpp",
"assert": "cpp",
"narrow": "cpp"
"narrow": "cpp",
"span_ext": "cpp",
"util": "cpp"
},
"files.trimFinalNewlines": true,
"files.insertFinalNewline": true,
Expand Down
123 changes: 122 additions & 1 deletion include/libtcod-fov/map.hpp
Original file line number Diff line number Diff line change
@@ -1,14 +1,135 @@
#include <memory>
#include <stdexcept>
#include <string>

#include "map_inline.h"
#include "map_types.h"

namespace tcod::fov {

/// @brief Deleter for TCODFOV map structs.
struct MapDeleter {
void operator()(TCODFOV_Map2D* map) const { TCODFOV_map2d_delete(map); }
};

/// @brief Unique pointer for `TCODFOV_Map2D`.
typedef std::unique_ptr<TCODFOV_Map2D, MapDeleter> Map2DPtr;

/// @brief A 2D bitpacked boolean array.
class Bitpacked2D {
public:
/// @brief Iterator over a bitpacked boolean array. Acts as both a reference and pointer.
class Bitpacked2DIter {
public:
Bitpacked2DIter() = default;
Bitpacked2DIter(Bitpacked2D* map, std::array<int, 2> ij) noexcept : map_{map}, ij_{std::move(ij)} {}

/// @brief Assign boolean to this index.
/// @throws std::out_of_range If iterator is out-of-bounds.
Bitpacked2DIter& operator=(bool value) {
map_->set_bool(ij_, value);
return *this;
}

/// @brief This class should act as a bool when possible.
/// @throws std::out_of_range If iterator is out-of-bounds.
operator bool() const { return map_->get_bool(ij_); }

/// @brief This object is both an iterator and a reference, so return itself.
Bitpacked2DIter& operator*() noexcept { return *this; }

/// @brief Increment along row-major order.
Bitpacked2DIter& operator++() noexcept {
++ij_[0];
while (ij_[0] > map_->map_.bitpacked.shape[0]) {
++ij_[1];
ij_[0] -= map_->map_.bitpacked.shape[0];
}
return *this;
}

bool operator==(const Bitpacked2DIter& other) const noexcept { return map_ == other.map_ && ij_ == other.ij_; }
bool operator!=(const Bitpacked2DIter& other) const noexcept { return !(*this == other); }

private:
Bitpacked2D* map_{}; // Pointer to this iterators map.
std::array<int, 2> ij_{}; // Index on the map.
};

/// @brief Default initialize an array with no shape.
Bitpacked2D() : Bitpacked2D({0, 0}) {}
/// @brief Create a new array with the given shape and inital value.
/// @param shape `{height, width}` shape of the new array.
/// @param init Inital value of the booleans in this array.
explicit Bitpacked2D(const std::array<int, 2>& shape, bool init = false)
: array_(shape[0] * (TCODFOV_round_to_byte_(shape[1])), (init ? 0xff : 0)),
map_{
.bitpacked{
.type = TCODFOV_MAP2D_BITPACKED,
.shape = {shape[0], shape[1]},
.data = array_.data(),
.y_stride = TCODFOV_round_to_byte_(shape[1]),
},
} {}

Bitpacked2D(const Bitpacked2D&) = delete;
Bitpacked2D& operator=(const Bitpacked2D&) = delete;
Bitpacked2D(Bitpacked2D&&) = default;
Bitpacked2D& operator=(Bitpacked2D&&) = default;

auto begin() noexcept { return Bitpacked2DIter{this, {0, 0}}; }
auto end() noexcept { return Bitpacked2DIter{this, {map_.bitpacked.shape[0], 0}}; }

/// @brief Return the shape of the array as `{height, width}`.
auto get_shape() const noexcept -> std::array<int, 2> { return {map_.bitpacked.shape[0], map_.bitpacked.shape[1]}; }

/// @brief Return the width of this array.
auto get_width() const noexcept -> int { return map_.bitpacked.shape[1]; }
/// @brief Return the height of this array.
auto get_height() const noexcept -> int { return map_.bitpacked.shape[0]; }

/// @brief Return the boolean at `ij`.
/// @throws std::out_of_range If `ij` is out-of-bounds.
auto get_bool(const std::array<int, 2>& ij) const -> bool {
check_bounds_(ij);
const uint8_t active_bit = 1 << (ij[1] % 8);
return (array_.at(map_.bitpacked.y_stride * ij[0] + (ij[1] / 8)) & active_bit) != 0;
}

/// @brief Set the boolean at `ij` to `value`.
/// @throws std::out_of_range If `ij` is out-of-bounds.
auto set_bool(const std::array<int, 2>& ij, bool value) -> void {
check_bounds_(ij);
const uint8_t active_bit = 1 << (ij[1] % 8);
auto& byte_ref = array_.at(map_.bitpacked.y_stride * ij[0] + (ij[1] / 8));
byte_ref = (byte_ref & ~active_bit) | (value ? active_bit : 0);
}

/// @brief Return true if `ij` is within the bounds of this array.
auto in_bounds(const std::array<int, 2>& ij) const noexcept -> bool {
return 0 <= ij[0] && 0 <= ij[1] && ij[0] < map_.bitpacked.shape[0] && ij[1] < map_.bitpacked.shape[1];
}

/// @brief Return the `TCODFOV_Map2D*` pointer for this object.
auto get_ptr() noexcept { return static_cast<TCODFOV_Map2D*>(*this); }
/// @brief Return the `const TCODFOV_Map2D*` pointer for this object.
auto get_ptr() const noexcept { return static_cast<const TCODFOV_Map2D*>(*this); }

/// @brief Conversion to `TCODFOV_Map2D*` so that this may be passed to C FOV functions.
explicit operator TCODFOV_Map2D*() noexcept { return &map_; }
/// @brief Conversion to `const TCODFOV_Map2D*` so that this may be passed to C FOV functions.
explicit operator const TCODFOV_Map2D*() const noexcept { return &map_; }

private:
/// @brief Throw `std::out_of_range` if `ij` is out-of-bounds.
void check_bounds_(const std::array<int, 2>& ij) const {
if (in_bounds(ij)) return;
throw std::out_of_range(
std::string("{") + std::to_string(ij[0]) + ", " + std::to_string(ij[1]) +
"} not in bounds of array of shape {" + std::to_string(map_.bitpacked.shape[0]) + ", " +
std::to_string(map_.bitpacked.shape[1]) + "}");
}
std::vector<uint8_t> array_{}; // Bitpacked map data.
TCODFOV_Map2D map_{}; // Map union in memory, includes a pointer to `array_`.
};

} // namespace tcod::fov
19 changes: 11 additions & 8 deletions src/fovtool/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
#include <string>

#include "libtcod-fov.h"
#include "libtcod-fov/libtcod_int.h"

struct MapInfo {
tcod::fov::TCODMap data; // Map tile data
tcod::fov::Bitpacked2D transparency; // Map tile data
tcod::fov::Bitpacked2D visible; // Tiles in FOV
std::vector<std::tuple<int, int>> sources{}; // POV/light sources
};

Expand All @@ -41,15 +43,16 @@ static auto load_map(const std::filesystem::path& path) {
const int map_width =
std::ranges::max(lines | std::views::transform([](const auto& line) { return gsl::narrow<int>(line.size()); }));
auto map = MapInfo{
.data = {map_width, map_height},
.transparency = tcod::fov::Bitpacked2D{{map_height, map_width}},
.visible = tcod::fov::Bitpacked2D{{map_height, map_width}},
};
for (int y = 0; y < gsl::narrow<int>(lines.size()); ++y) {
const auto& line = lines.at(y);
for (int x = 0; x < map_width; ++x) {
static constexpr auto DEFAULT_CH = '.';
const auto ch = (x < gsl::narrow<int>(line.size()) ? line.at(x) : DEFAULT_CH);
const bool transparent = ch != '#';
map.data.setProperties(x, y, transparent, false);
map.transparency.set_bool({y, x}, transparent);
if (ch == '@') {
map.sources.push_back({x, y});
}
Expand All @@ -60,15 +63,15 @@ static auto load_map(const std::filesystem::path& path) {

static auto render_map(const MapInfo& map) {
auto stream = std::ostringstream{};
for (int y = 0; y < map.data.getHeight(); ++y) {
for (int y = 0; y < map.transparency.get_height(); ++y) {
if (y) stream << '\n';
for (int x = 0; x < map.data.getWidth(); ++x) {
for (int x = 0; x < map.transparency.get_width(); ++x) {
if (std::ranges::any_of(map.sources, [=](const auto& xy) { return xy == std::tuple<int, int>{x, y}; })) {
stream << '@';
continue;
}
const bool visible = map.data.isInFov(x, y);
const bool transparent = map.data.isTransparent(x, y);
const bool visible = map.visible.get_bool({y, x});
const bool transparent = map.transparency.get_bool({y, x});
stream << (visible ? (transparent ? '.' : '#') : (transparent ? ' ' : ' '));
}
}
Expand All @@ -93,7 +96,7 @@ int main(int argc, char** argv) {
try {
auto map = load_map(input_str);
for (const auto& [x, y] : map.sources) {
TCODFOV_map_compute_fov(map.data.data, x, y, 0, true, TCODFOV_SYMMETRIC_SHADOWCAST);
TCODFOV_map_compute_fov_symmetric_shadowcast(map.transparency.get_ptr(), map.visible.get_ptr(), x, y, 0, true);
std::cout << render_map(map) << '\n';
}
} catch (const std::exception& e) {
Expand Down
54 changes: 22 additions & 32 deletions tests/test_fov.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

struct MapInfo {
std::string name{};
tcod::fov::Map2DPtr data{}; // Map tile data
tcod::fov::Bitpacked2D data{}; // Map tile data
std::tuple<int, int> pov{0, 0};
};

Expand All @@ -36,15 +36,15 @@ static auto load_map(std::string name, const std::filesystem::path& path) {
std::ranges::max(lines | std::views::transform([](const auto& line) { return gsl::narrow<int>(line.size()); }));
auto map = MapInfo{
.name = std::move(name),
.data = tcod::fov::Map2DPtr{TCODFOV_map2d_new_bitpacked(map_width, map_height)},
.data = tcod::fov::Bitpacked2D{{map_height, map_width}},
};
for (int y = 0; y < gsl::narrow<int>(lines.size()); ++y) {
const auto& line = lines.at(y);
for (int x = 0; x < map_width; ++x) {
static constexpr auto DEFAULT_CH = '.';
const auto ch = (x < gsl::narrow<int>(line.size()) ? line.at(x) : DEFAULT_CH);
const bool transparent = ch != '#';
TCODFOV_map2d_set_bool(map.data.get(), x, y, transparent);
map.data.set_bool({y, x}, transparent);
if (ch == '@') {
map.pov = {x, y};
}
Expand All @@ -53,36 +53,27 @@ static auto load_map(std::string name, const std::filesystem::path& path) {
return map;
}

static auto new_map_with_radius(int radius, bool start_transparent) -> tcod::fov::Map2DPtr {
static auto new_map_with_radius(int radius, bool start_transparent) -> tcod::fov::Bitpacked2D {
const int size = radius * 2 + 1;
tcod::fov::Map2DPtr map{TCODFOV_map2d_new_bitpacked(size, size)};
for (int y = 0; y < TCODFOV_map2d_get_height(map.get()); ++y) {
for (int x = 0; x < TCODFOV_map2d_get_height(map.get()); ++x) {
TCODFOV_map2d_set_bool(map.get(), x, y, start_transparent);
}
}
tcod::fov::Bitpacked2D map{{size, size}, start_transparent};
return map;
}
static auto new_empty_map(int radius) -> tcod::fov::Map2DPtr { return new_map_with_radius(radius, true); }
static auto new_opaque_map(int radius) -> tcod::fov::Map2DPtr { return new_map_with_radius(radius, false); }
static auto new_corridor_map(int radius) -> tcod::fov::Map2DPtr {
tcod::fov::Map2DPtr map{new_map_with_radius(radius, false)};
static auto new_empty_map(int radius) -> tcod::fov::Bitpacked2D { return new_map_with_radius(radius, true); }
static auto new_opaque_map(int radius) -> tcod::fov::Bitpacked2D { return new_map_with_radius(radius, false); }
static auto new_corridor_map(int radius) -> tcod::fov::Bitpacked2D {
tcod::fov::Bitpacked2D map{new_map_with_radius(radius, false)};
for (int i = 0; i < radius * 2 + 1; ++i) {
TCODFOV_map2d_set_bool(map.get(), radius, i, true);
TCODFOV_map2d_set_bool(map.get(), i, radius, true);
map.set_bool({i, radius}, true);
map.set_bool({radius, i}, true);
}
return map;
}
static auto new_forest_map(int radius) -> tcod::fov::Map2DPtr {
static auto new_forest_map(int radius) -> tcod::fov::Bitpacked2D {
// Forest map with 1 in 4 chance of a blocking tile.
std::mt19937 rng(0);
std::uniform_int_distribution<int> chance(0, 3);
tcod::fov::Map2DPtr map{new_map_with_radius(radius, true)};
for (int y = 0; y < TCODFOV_map2d_get_height(map.get()); ++y) {
for (int x = 0; x < TCODFOV_map2d_get_height(map.get()); ++x) {
if (chance(rng) == 0) TCODFOV_map2d_set_bool(map.get(), x, y, false);
}
}
tcod::fov::Bitpacked2D map{new_map_with_radius(radius, true)};
for (auto& it : map) it = (chance(rng) == 0);
return map;
}

Expand Down Expand Up @@ -110,27 +101,26 @@ TEST_CASE("FOV Benchmarks", "[.benchmark]") {
};
for (auto& active_test : test_maps) {
const std::string& map_name = active_test.name;
tcod::fov::Map2DPtr& map = active_test.data;
tcod::fov::Map2DPtr out = tcod::fov::Map2DPtr{
TCODFOV_map2d_new_bitpacked(TCODFOV_map2d_get_width(map.get()), TCODFOV_map2d_get_height(map.get()))};
const auto& map = active_test.data;
auto out = tcod::fov::Bitpacked2D{map.get_shape()};
const auto [pov_x, pov_y] = active_test.pov;
BENCHMARK(map_name + " TCODFOV_BASIC") {
(void)!TCODFOV_map_compute_fov_circular_raycasting(map.get(), out.get(), pov_x, pov_y, 0, true);
(void)!TCODFOV_map_compute_fov_circular_raycasting(map.get_ptr(), out.get_ptr(), pov_x, pov_y, 0, true);
};
BENCHMARK(map_name + " TCODFOV_DIAMOND") {
(void)!TCODFOV_map_compute_fov_diamond_raycasting(map.get(), out.get(), pov_x, pov_y, 0, true);
(void)!TCODFOV_map_compute_fov_diamond_raycasting(map.get_ptr(), out.get_ptr(), pov_x, pov_y, 0, true);
};
BENCHMARK(map_name + " TCODFOV_SHADOW") {
(void)!TCODFOV_map_compute_fov_recursive_shadowcasting(map.get(), out.get(), pov_x, pov_y, 0, true);
(void)!TCODFOV_map_compute_fov_recursive_shadowcasting(map.get_ptr(), out.get_ptr(), pov_x, pov_y, 0, true);
};
BENCHMARK(map_name + " TCODFOV_RESTRICTIVE") {
(void)!TCODFOV_map_compute_fov_restrictive_shadowcasting(map.get(), out.get(), pov_x, pov_y, 0, true);
(void)!TCODFOV_map_compute_fov_restrictive_shadowcasting(map.get_ptr(), out.get_ptr(), pov_x, pov_y, 0, true);
};
BENCHMARK(map_name + " TCODFOV_PERMISSIVE_8") {
(void)!TCODFOV_map_compute_fov_permissive2(map.get(), out.get(), pov_x, pov_y, 0, true, 8);
(void)!TCODFOV_map_compute_fov_permissive2(map.get_ptr(), out.get_ptr(), pov_x, pov_y, 0, true, 8);
};
BENCHMARK(map_name + " TCODFOV_SYMMETRIC_SHADOWCAST") {
(void)!TCODFOV_map_compute_fov_symmetric_shadowcast(map.get(), out.get(), pov_x, pov_y, 0, true);
(void)!TCODFOV_map_compute_fov_symmetric_shadowcast(map.get_ptr(), out.get_ptr(), pov_x, pov_y, 0, true);
};
}
}

0 comments on commit d80f6f8

Please sign in to comment.