#include "renderer.hpp"
#include <assert.h>
#include <algorithm>
#include <cmath>
#include "texture.hpp"
#include "xwin.hpp"
/** @brief @ref Buffer2D proxy base class, implementing @ref Sampler access. */
template <class T>
class BufferView {
protected:
const res_t res;
const Buffer2D<T>* buf;
public:
BufferView(res_t res) : res{std::min(res.w, res.h) / 2 * 2, std::min(res.w, res.h) / 2 * 2}, buf(nullptr) {
assert(res.x >= 2 && res.y >= 2);
}
const res_t& resolution_in() const {
return res;
};
/** @brief set underlying buffer instance */
void attach(const Buffer2D<T>* newbuf) {
assert(newbuf->width() == res.w && newbuf->height() == res.h);
assert(buf == nullptr);
buf = newbuf;
}
/** @brief remove binding to buffer instance */
void release() {
assert(buf != nullptr);
buf = nullptr;
}
/** @brief output dimension, e.g, doubled when subsampling */
virtual res_t resolution_out() const = 0;
/** @brief input coordinates to output coordinates */
virtual area_t translate(const area_t&) const = 0;
/** @brief actual buffer access, possibly interpolated */
virtual T at(dcoord_t x, dcoord_t y) const = 0;
};
/** @brief Same resolution noop, directly proxy buffer access. */
class BufferIdentityView final : public BufferView<vertex_t> {
public:
using BufferView::BufferView;
res_t resolution_out() const override {
return resolution_in();
}
area_t translate(const area_t& box) const override {
return box;
}
INLINE vertex_t at(dcoord_t x, dcoord_t y) const override {
return buf->at(x, y);
}
};
/** @brief Nearest neighbour interpolation, doubling resolution. */
class BufferNearestView final : public BufferView<vertex_t> {
public:
using BufferView::BufferView;
res_t resolution_out() const override {
return {res.w * 2, res.h * 2};
}
area_t translate(const area_t& box) const override {
return area_t{{box.lo.x * 2, box.lo.y * 2}, {box.hi.x * 2 + 1, box.hi.y * 2 + 1}};
}
INLINE vertex_t at(dcoord_t x, dcoord_t y) const override {
return buf->at(x / 2, y / 2); // round down
}
};
/** @brief Double resolution without any interpolation. */
class BufferDitherView final : public BufferView<vertex_t> {
public:
using BufferView::BufferView;
res_t resolution_out() const override {
return {res.w * 2, res.h * 2};
}
area_t translate(const area_t& box) const override {
return area_t{{box.lo.x * 2, box.lo.y * 2}, {box.hi.x * 2 + 1, box.hi.y * 2 + 1}};
}
INLINE vertex_t at(dcoord_t x, dcoord_t y) const override {
return (x % 2 == 0 && y % 2 == 0) ? buf->at(x / 2, y / 2) : zeroes;
}
};
/** @brief Bilinear interpolation. */
class BufferBiLinView final : public BufferView<vertex_t> {
public:
using BufferView::BufferView;
res_t resolution_out() const override {
return {res.w * 2 - 1, res.h * 2 - 1};
}
area_t translate(const area_t& box) const override {
return area_t{{std::min(this->resolution_out().w - 1, std::max(0, box.lo.x * 2 - 1)),
std::min(this->resolution_out().h - 1, std::max(0, box.lo.y * 2 - 1))},
{std::min(this->resolution_out().w - 1, box.hi.x * 2 + 1),
std::min(this->resolution_out().h - 1, box.hi.y * 2 + 1)}};
}
INLINE vertex_t at(dcoord_t x, dcoord_t y) const override {
const dcoord_t u = x / 2; // round down
const dcoord_t v = y / 2;
switch (y & 1) { // y % 2
case 0:
switch (x & 1) {
case 0:
return buf->at(u, v);
default:
return (buf->at(u, v) + buf->at(u + 1, v)) / 2.0F;
}
default:
switch (x & 1) {
case 0:
return (buf->at(u, v) + buf->at(u, v + 1)) / 2.0F;
default:
return (buf->at(u, v) + buf->at(u + 1, v) + buf->at(u, v + 1) + buf->at(u + 1, v + 1)) / 4.0F;
}
}
}
};
Sampler::Sampler(res_t resolution, bool subsample) : res(resolution), view(make(resolution, subsample)) {
LOG("Sampling %d/%d of %d/%d px (+ %d/%d)", resolution_in().w, resolution_in().h, resolution_out().w,
resolution_out().h, resolution_off().w, resolution_off().h);
}
// NB: need destructor defined where BufferView is known, even if merely = default
Sampler::~Sampler() = default;
std::unique_ptr<BufferView<vertex_t>> Sampler::make(res_t res, bool subsample) {
if (subsample) {
#if USE_UPSAMPLE
return std::make_unique<BufferBiLinView>(res_t{res.w / 2, res.h / 2});
#else
return std::make_unique<BufferNearestView>(res_t{res.w / 2, res.h / 2});
#endif
} else {
return std::make_unique<BufferIdentityView>(res);
}
}
BufferView<vertex_t>* Sampler::get() {
return view.get();
}
res_t Sampler::resolution_in() const {
return view->resolution_in();
}
res_t Sampler::resolution_off() const {
return res_t{(res.w - resolution_out().w) / 2, (res.h - resolution_out().h) / 2};
}
res_t Sampler::resolution_out() const {
return view->resolution_out();
}
template <class T>
void SlidingAverage<T>::push(T value) {
++total_num;
total_sum += value;
total_avg = static_cast<float>(total_sum) / static_cast<float>(total_num);
if (buf.size() < len) {
buf.push_back(value);
} else { // move linked list node instead of delete + new
sum -= buf.front();
buf.splice(buf.end(), buf, buf.begin());
buf.back() = value;
}
sum += value;
avg = static_cast<float>(sum) / static_cast<float>(buf.size());
}
/**
* @brief Get color information from hit texture position and apply light.
*
* Track and return 'dirty' columns/rows that have nonzero content.
*/
static area_t draw_light(const Buffer2D<trace_px_t>& trace_info, Buffer2D<vertex_t>& light_info) {
area_t dirty{{light_info.width() - 1, light_info.height() - 1}, {0, 0}};
for (dcoord_t y = 0; likely(y < light_info.height()); ++y) {
for (dcoord_t x = 0; likely(x < light_info.width()); ++x) {
const trace_px_t& px = trace_info.at(x, y);
vertex_t& light = light_info.at(x, y);
if (px.obj == nullptr || px.tex == nullptr || light <= zeroes) {
light.set(0.0F);
continue;
}
dirty.minmax(x, y);
#if DEPTH_MAP
light.set(std::min((0.9F * px.hit.z + 0.1F) * 255.0F, 255.0F));
#else
light = vertex_t::clamp(light * px.tex->get_color(px.uv, px.hit) * charmax, zeroes, charmax);
#endif
}
}
return dirty;
}
/** @brief Given sampling/interpolation buffer to final result, convert to X image RGB values. */
static void draw_frame_corner(Renderer::render_task_t* task, size_t corner) {
Timer timer;
const area_t& viewport = task->context.corners.at(corner).viewport;
const area_t& dirty = task->frame.dirty_box();
const area_t window = task->view.translate({viewport.lo, {viewport.hi.x - 1, viewport.hi.y - 1}});
const vertex2_t<dcoord_t> start = vertex2_t<dcoord_t>::maxof(dirty.lo, window.lo);
const vertex2_t<dcoord_t> end = vertex2_t<dcoord_t>::minof(dirty.hi, window.hi);
const BufferView<vertex_t>& view = task->view;
XFrame& frame = task->frame;
assert(view.resolution_out().w == frame.resolution().w && view.resolution_out().h == frame.resolution().h);
for (dcoord_t y = start.y; likely(y <= end.y); ++y) {
const dcoord_t yy = frame.resolution().h - y - 1; // account for upper origin in X
for (dcoord_t x = start.x; likely(x <= end.x); ++x) {
const vertex_t light = view.at(x, y); //.rounded();
#if USE_PPM
frame.at(x, yy) = {.r = static_cast<unsigned char>(light.r),
.g = static_cast<unsigned char>(light.g),
.b = static_cast<unsigned char>(light.b)};
#else
frame.at(x, yy) = {.b = static_cast<unsigned char>(light.b),
.g = static_cast<unsigned char>(light.g),
.r = static_cast<unsigned char>(light.r)};
#endif
}
}
#if USE_LOG_STATS > 2
LOG("Render in %4lu ms (%d * %d px @ %zu)", timer.measure(), end.x - start.x, end.y - start.y, corner);
#endif
}
Renderer::Renderer(ContextFactory& context, Sampler& sampler)
: frame_duration(100),
sampler(sampler),
xwin(std::make_unique<XWin>(sampler.resolution_out(), sampler.resolution_off())),
thread(context, [this](std::unique_ptr<context_t>&& f) { draw(std::move(f)); }),
threads(&draw_frame_corner) {}
Renderer::~Renderer() {
LOG("Total %zu frames: %.3f ms avg, %.3f frames/s", frame_duration.get_num(), frame_duration.get_avg(),
1000.0F / frame_duration.get_avg());
LOG("Last %zu frames: %.3f ms avg, %.3f frames/s", frame_duration.get_size(), frame_duration.get_sliding_avg(),
1000.0F / frame_duration.get_sliding_avg());
};
const SlidingAverage<msec_t>& Renderer::duration_hint() const {
return frame_duration;
}
ThreadQueue<context_t>& Renderer::queue() {
return thread;
}
void Renderer::draw(std::unique_ptr<context_t>&& ctx) {
Timer timer;
const area_t dirty = draw_light(ctx->trace_info, ctx->light_info);
#if USE_LOG_STATS > 2
LOG("Render in %4lu ms (illumination)", timer.measurement());
#endif
BufferView<vertex_t>& view = *sampler.get();
view.attach(&ctx->light_info);
XFrame& frame = xwin->get_frame();
area_t& frame_dirty = frame.dirty_box();
const area_t scene_dirty = view.translate(dirty);
frame_dirty.minmax(scene_dirty); // merge
render_task_t task{*ctx, view, frame};
if (threads.run(&task)) {
#if USE_LOG_STATS > 2
LOG("RENDER in %4lu ms (%d * %d px)", timer.measurement(), frame_dirty.hi.x - frame_dirty.lo.x,
frame_dirty.hi.y - frame_dirty.lo.y);
#endif
frame_dirty = scene_dirty;
xwin->draw();
#if USE_LOG_STATS > 1
LOG("Render in %4lu ms (draw)", timer.measurement());
#endif
}
view.release();
#if USE_LOG_STATS > 0
LOG("Sum Delay %4lu ms (pipeline)", ctx->timer.measure());
#endif
frame_duration.push(ctx->timer.measure());
thread.dispose(std::move(ctx));
}
Renderer::input_t Renderer::get_input(fcoord_t sensitivity) {
input_t in{};
XKeyEvents& keys = xwin->get_keys();
keys.wait(); // NB: no timeout
XKeyEvents::action_t actions{};
bool moved = keys.pop_movement(sensitivity, in.offset);
keys.pop_action(actions);
// NOLINTNEXTLINE(readability-implicit-bool-conversion)
in.action = {moved, 0, actions.action, actions.restart, actions.regen, actions.quit};
return in;
}