pretty-automata/src/program.cpp

1100 lines
42 KiB
C++

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include "colormaps/colormaps.h"
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include "imgui_internal.h"
#include "options.h"
#include "program.h"
#include "shader-helper.h"
#include <chrono>
#include <cstring>
#include <format>
#include <iostream>
#include <queue>
#include <thread>
#include <utility>
#include <vector>
#include <embedded-resources/fonts.h>
#include <embedded-resources/shaders.h>
static const GLfloat g_vertex_buffer_data[] = {
-1.0f,
-1.0f,
1.0f,
-1.0f,
-1.0f,
1.0f,
1.0f,
1.0f,
};
enum ProgramState {
WAIT,
RENDER_FRAME,
START_COMPUTE,
COMPUTE_FINISHED,
RENDER_FINISHED,
NEXT_GENERATION,
};
static void GLAPIENTRY openGlMessageCallback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *message, const void *userParam) {
throw std::runtime_error(message);
}
static void glfwErrorCallback(int error, const char *description) {
throw std::runtime_error(description);
}
static void glfwKeyCallback(GLFWwindow *window, int key, int scancode, int action, int mods) {
auto instance = static_cast<Program *>(glfwGetWindowUserPointer(window));
instance->keyCallback(key, scancode, action, mods);
}
static void glfwFramebufferResizeCallback(GLFWwindow *window, int width, int height) {
glViewport(0, 0, width, height);
}
Program::Program(Options opts) : options(std::move(opts)) {
glfwSetErrorCallback(glfwErrorCallback);
if (!glfwInit()) {
throw std::runtime_error("Failed to initialize GLFW");
}
createWindow();
glfwSetWindowUserPointer(window, this);
if (glewInit() != GLEW_OK) {
throw std::runtime_error("Failed to initialize GLEW");
}
float scale_x, scale_y;
glfwGetWindowContentScale(window, &scale_x, &scale_y);
float scale = std::max(scale_x, scale_y);
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO &io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.IniFilename = nullptr;
// Setup Dear ImGui style
// ImGui::StyleColorsDark();
ImGui::StyleColorsLight();
// Setup Platform/Renderer backends
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init("#version 150");
ImFontConfig fontCfg;
fontCfg.FontDataOwnedByAtlas = false;
auto dejavusansmono = resources::fonts::DejaVuSansMono();
io.Fonts->AddFontFromMemoryTTF((void *)dejavusansmono.data(), static_cast<int>(dejavusansmono.size()), 16.f * scale, &fontCfg);
ImGuiStyle &style = ImGui::GetStyle();
style.ScaleAllSizes(scale);
glEnable(GL_DEBUG_OUTPUT);
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
glDebugMessageCallback(openGlMessageCallback, nullptr);
// std::cout << automaton_compute_shader_source_len << std::endl;
auto compute_shader_source = resources::shaders::automaton_comp();
auto vertex_shader_source = resources::shaders::automaton_vert();
auto fragment_shader_source = resources::shaders::automaton_frag();
renderProgram = loadProgram("render", {
loadShader("vertex", vertex_shader_source, GL_VERTEX_SHADER),
loadShader("fragment", fragment_shader_source, GL_FRAGMENT_SHADER),
});
computeProgram = loadProgram("compute", {loadShader("compute", compute_shader_source, GL_COMPUTE_SHADER)});
glUseProgram(computeProgram);
computeBlockIndexInput = glGetProgramResourceIndex(computeProgram, GL_SHADER_STORAGE_BLOCK, "buffer_data_in");
computeBlockIndexOutput = glGetProgramResourceIndex(computeProgram, GL_SHADER_STORAGE_BLOCK, "buffer_data_out");
computeDimensionsLoc = glGetUniformLocation(computeProgram, "dimensions");
computeBirthRuleLoc = glGetUniformLocation(computeProgram, "birth_rule");
computeSurviveRuleLoc = glGetUniformLocation(computeProgram, "survive_rule");
computeStarveDelayLoc = glGetUniformLocation(computeProgram, "starve_delay");
computeStarveRecoverLoc = glGetUniformLocation(computeProgram, "starve_recover");
computeHasMaxAgeLoc = glGetUniformLocation(computeProgram, "has_max_age");
glUniform2ui(computeDimensionsLoc, options.automaton_options.width, options.automaton_options.height);
glUseProgram(renderProgram);
renderBlockIndexFrom = glGetProgramResourceIndex(renderProgram, GL_SHADER_STORAGE_BLOCK, "buffer_data_from");
renderBlockIndexTo = glGetProgramResourceIndex(renderProgram, GL_SHADER_STORAGE_BLOCK, "buffer_data_to");
renderPreviewColormapLoc = glGetUniformLocation(renderProgram, "preview_colormap");
renderDimensionsLoc = glGetUniformLocation(renderProgram, "dimensions");
renderShowMaxAgeLoc = glGetUniformLocation(renderProgram, "show_max_age");
renderGlobalMaxAgeLoc = glGetUniformLocation(renderProgram, "global_max_age");
renderGlobalMinimumMaxAgeLoc = glGetUniformLocation(renderProgram, "global_minimum_max_age");
renderBlendStepLoc = glGetUniformLocation(renderProgram, "blend_step");
renderDeadColorLoc = glGetUniformLocation(renderProgram, "dead_color");
renderDeadColorIsCielabLoc = glGetUniformLocation(renderProgram, "dead_color_is_cielab");
renderLivingColorLoc = glGetUniformLocation(renderProgram, "living_color");
renderLivingColormapLoc = glGetUniformLocation(renderProgram, "living_colormap");
renderLivingUseColormapLoc = glGetUniformLocation(renderProgram, "living_use_colormap");
renderLivingColormapInvertLoc = glGetUniformLocation(renderProgram, "living_colormap_invert");
renderLivingColormapScaleLoc = glGetUniformLocation(renderProgram, "living_colormap_scale");
renderLivingColormapScaleIsMaxAgeLoc = glGetUniformLocation(renderProgram, "living_colormap_scale_is_max_age");
renderLivingColorIsCielabLoc = glGetUniformLocation(renderProgram, "living_color_is_cielab");
glUniform2ui(renderDimensionsLoc, options.automaton_options.width, options.automaton_options.height);
ColorMapData custom_colormap_data{};
if (std::holds_alternative<ColorMapOption>(options.display_options.alive_color)) {
auto cmo = std::get<ColorMapOption>(options.display_options.alive_color);
if (std::holds_alternative<ColorMapData>(cmo.source)) {
custom_colormap_data = std::get<ColorMapData>(cmo.source);
}
}
auto colormaps_ = colormaps;
colormaps_["<custom>"] = custom_colormap_data;
auto num_colormaps = static_cast<int>(colormaps_.size());
GLuint textureIds[num_colormaps];
glGenTextures(num_colormaps, textureIds);
int i = 0;
for (const auto &colormap : colormaps_) {
GLuint textureId = textureIds[i];
colormap_textures[colormap.first] = textureId;
glBindTexture(GL_TEXTURE_1D, textureId);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage1D(GL_TEXTURE_1D, 0, GL_RGB32F, static_cast<GLsizei>(colormap.second.size()), 0, GL_RGB, GL_FLOAT, colormap.second.data());
i++;
}
glBindTexture(GL_TEXTURE_1D, 0);
GLuint vertexArrayId;
glGenVertexArrays(1, &vertexArrayId);
glBindVertexArray(vertexArrayId);
glGenBuffers(1, &vertexBuffer);
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
glBufferData(GL_ARRAY_BUFFER, sizeof(g_vertex_buffer_data), g_vertex_buffer_data, GL_STATIC_DRAW);
}
Program::~Program() {
glfwTerminate();
}
void Program::run() {
auto bufferDataSize = static_cast<GLsizeiptr>(sizeof(uint32_t) * options.automaton_options.width * options.automaton_options.height * 2); // 2x for data and max-age
constexpr int numBuffers = 10;
std::vector<BufferInfo> free_buffers(numBuffers);
std::queue<BufferInfo> filled_buffers{};
{
int bindingPoint = 1;
for (auto &item : free_buffers) {
item.bindingPoint = bindingPoint++;
glGenBuffers(1, &item.bufferName);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, item.bufferName);
glBufferData(GL_SHADER_STORAGE_BUFFER, bufferDataSize, nullptr, GL_DYNAMIC_COPY);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, item.bindingPoint, item.bufferName);
}
}
BufferInfo currentComputeBufferIn{}, currentComputeBufferOut{}, currentRenderBufferFrom{}, currentRenderBufferTo{};
currentComputeBufferIn = free_buffers.back();
free_buffers.pop_back();
currentComputeBufferOut = free_buffers.back();
free_buffers.pop_back();
initializeFirstBuffer(currentComputeBufferIn);
initializeMaxAge(currentComputeBufferIn, 0);
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
debugPrintBuffer("initialize", currentComputeBufferIn);
filled_buffers.push(currentComputeBufferIn);
enum SyncType {
RENDER,
COMPUTE
};
std::queue<std::pair<GLsync, SyncType>> fences{};
{
auto computeFence = startCompute(currentComputeBufferIn, currentComputeBufferOut);
GLenum syncResult;
do {
syncResult = glClientWaitSync(computeFence, 0, std::chrono::nanoseconds(std::chrono::seconds(1)).count());
} while (syncResult == GL_TIMEOUT_EXPIRED);
glDeleteSync(computeFence);
filled_buffers.push(currentComputeBufferOut);
debugPrintBuffer("first compute", currentComputeBufferOut);
currentComputeBufferIn = currentComputeBufferOut;
currentComputeBufferOut = free_buffers.back();
free_buffers.pop_back();
computeFence = startCompute(currentComputeBufferIn, currentComputeBufferOut);
fences.emplace(computeFence, COMPUTE);
}
currentRenderBufferFrom = filled_buffers.front();
filled_buffers.pop();
currentRenderBufferTo = filled_buffers.front();
filled_buffers.pop();
const bool max_age_is_dynamic = options.automaton_options.rule.max_age != nullptr && options.automaton_options.rule.max_age->is_dynamic();
uint16_t max_age_dynamic_step{};
if (max_age_is_dynamic) {
max_age_dynamic_step = options.automaton_options.rule.max_age->dynamic_step();
}
using frames = std::chrono::duration<int32_t, std::ratio<1, 60>>;
auto start_point = std::chrono::steady_clock::now();
uint64_t generation_count = 1;
uint64_t compute_generation_count = 1;
uint64_t frame_count = 1;
auto next_frame_point = std::optional(start_point + frames{frame_count});
bool computeIsRunning = true;
ProgramState state = WAIT;
int renderCount = 0;
int computeCount = 0;
bool waiting_for_compute = false;
while (glfwWindowShouldClose(window) == 0) {
const auto generation_duration = std::chrono::milliseconds(options.display_options.generation_duration_ms);
// TODO: calculate next point from previous point, currently changing the duration fucks up everything
// or maybe changing any of the automaton settings should restart it
auto next_gen_point = start_point + generation_count * generation_duration;
switch (state) {
case WAIT: {
// if (renderCount >= 2 && computeCount >= 2) return;
glfwPollEvents();
std::chrono::time_point<std::chrono::steady_clock, std::chrono::nanoseconds> next_time_point = next_gen_point;
if (next_frame_point.has_value() && next_frame_point.value() < next_gen_point) {
next_time_point = std::chrono::time_point_cast<std::chrono::nanoseconds>(next_frame_point.value());
}
bool wakeup_is_timer;
if (fences.empty()) {
// thread sleep
std::this_thread::sleep_until(next_time_point);
wakeup_is_timer = true;
} else {
GLsync fence = fences.front().first;
auto durationNanos = std::chrono::duration_cast<std::chrono::nanoseconds>(next_time_point - std::chrono::steady_clock::now()).count();
if (durationNanos < 0) {
durationNanos = 0;
}
if (waiting_for_compute) {
durationNanos = std::chrono::nanoseconds(std::chrono::milliseconds(100)).count();
}
auto res = glClientWaitSync(fence, 0, durationNanos);
wakeup_is_timer = res == GL_TIMEOUT_EXPIRED;
}
if (wakeup_is_timer) {
if (waiting_for_compute) {
state = WAIT;
} else {
if (next_time_point == next_gen_point) {
state = NEXT_GENERATION;
} else {
state = RENDER_FRAME;
}
}
} else {
auto fence = fences.front();
if (fence.second == COMPUTE) {
state = COMPUTE_FINISHED;
} else {
state = RENDER_FINISHED;
}
glDeleteSync(fence.first);
fences.pop();
}
} break;
case RENDER_FRAME: {
double step;
if (options.display_options.blend) {
auto gen_dur_float = std::chrono::duration_cast<std::chrono::duration<double>>(generation_duration);
auto dur_until_now = std::chrono::duration_cast<std::chrono::duration<double>>(std::chrono::steady_clock::now() - (start_point + (generation_count - 1) * generation_duration));
step = dur_until_now / gen_dur_float;
} else {
step = 0;
}
renderCount++;
renderFrame(currentRenderBufferFrom, currentRenderBufferTo, step);
renderUI();
auto fence = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
fences.emplace(fence, RENDER);
next_frame_point = std::nullopt;
state = WAIT;
} break;
case COMPUTE_FINISHED: {
computeIsRunning = false;
filled_buffers.push(currentComputeBufferOut);
debugPrintBuffer("compute complete", currentComputeBufferOut);
computeCount++;
if (waiting_for_compute) {
waiting_for_compute = false;
state = NEXT_GENERATION;
} else {
state = START_COMPUTE;
}
} break;
case START_COMPUTE: {
if (!computeIsRunning) {
if (!free_buffers.empty()) {
currentComputeBufferIn = currentComputeBufferOut;
compute_generation_count += 1;
if (max_age_is_dynamic && compute_generation_count % max_age_dynamic_step == 0) {
initializeMaxAge(currentComputeBufferIn, compute_generation_count);
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
}
currentComputeBufferOut = free_buffers.back();
free_buffers.pop_back();
auto fence = startCompute(currentComputeBufferIn, currentComputeBufferOut);
fences.emplace(fence, COMPUTE);
computeIsRunning = true;
} else if (waiting_for_compute) {
throw std::runtime_error("deadlock detected: waiting for compute, but compute is blocked");
}
}
state = WAIT;
} break;
case RENDER_FINISHED: {
glfwSwapBuffers(window);
auto now = std::chrono::steady_clock::now();
do {
// increment frame count, skip frames if necessary
frame_count += 1;
next_frame_point = start_point + frames{frame_count};
} while (next_frame_point < now);
state = WAIT;
} break;
case NEXT_GENERATION: {
if (!filled_buffers.empty()) {
free_buffers.push_back(currentRenderBufferFrom);
currentRenderBufferFrom = currentRenderBufferTo;
currentRenderBufferTo = filled_buffers.front();
filled_buffers.pop();
generation_count += 1;
next_gen_point = start_point + generation_count * generation_duration;
} else {
waiting_for_compute = true;
std::cerr << "warning: trying to render next generation but not yet computed" << std::endl
<< std::flush;
}
state = START_COMPUTE;
} break;
}
}
}
void Program::createWindow() {
glfwWindowHint(GLFW_SAMPLES, 0);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE);
constexpr auto window_title = PROGRAM_NAME " " PROGRAM_VERSION;
window_pos_x = 0;
window_pos_y = 0;
window_width = options.display_options.width;
window_height = options.display_options.height;
if (options.display_options.fullscreen) {
int num_monitors;
auto monitors = glfwGetMonitors(&num_monitors);
if (num_monitors == 0 || monitors == nullptr) {
throw std::runtime_error("glfwGetMonitors failed");
}
if (options.display_options.fullscreen_screen_number >= num_monitors) {
throw std::runtime_error(std::format("no screen with number {}", options.display_options.fullscreen_screen_number));
}
fullscreenMonitor = monitors[options.display_options.fullscreen_screen_number];
const GLFWvidmode *mode = glfwGetVideoMode(fullscreenMonitor);
glfwWindowHint(GLFW_RED_BITS, mode->redBits);
glfwWindowHint(GLFW_GREEN_BITS, mode->greenBits);
glfwWindowHint(GLFW_BLUE_BITS, mode->blueBits);
glfwWindowHint(GLFW_REFRESH_RATE, mode->refreshRate);
window = glfwCreateWindow(mode->width, mode->height, window_title, fullscreenMonitor, nullptr);
is_fullscreen = true;
} else {
window = glfwCreateWindow(window_width, window_height, window_title, nullptr, nullptr);
}
if (window == nullptr) {
throw std::runtime_error("Failed to open GLFW window");
}
glfwMakeContextCurrent(window);
glfwSetKeyCallback(window, glfwKeyCallback);
glfwSetFramebufferSizeCallback(window, glfwFramebufferResizeCallback);
}
uint32_t Program::makeRuleBitfield(const std::vector<uint8_t> &rule) const {
uint32_t bitfield = 0;
for (const auto &item : rule) {
bitfield |= 1 << item;
}
return bitfield;
}
GLsync Program::startCompute(BufferInfo in, BufferInfo out) const {
glUseProgram(computeProgram);
glUniform1ui(computeBirthRuleLoc, makeRuleBitfield(options.automaton_options.rule.birth));
glUniform1ui(computeSurviveRuleLoc, makeRuleBitfield(options.automaton_options.rule.survive));
glUniform1ui(computeStarveDelayLoc, options.automaton_options.rule.starve_delay);
glUniform1ui(computeStarveRecoverLoc, options.automaton_options.rule.starve_recover ? 1 : 0);
glUniform1ui(computeHasMaxAgeLoc, options.automaton_options.rule.max_age != nullptr);
glShaderStorageBlockBinding(computeProgram, computeBlockIndexInput, in.bindingPoint);
glShaderStorageBlockBinding(computeProgram, computeBlockIndexOutput, out.bindingPoint);
glDispatchCompute(options.automaton_options.width, options.automaton_options.height, 1);
return glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
}
void Program::renderFrame(BufferInfo from, BufferInfo to, double step) const {
glUseProgram(renderProgram);
glUniform1ui(renderShowMaxAgeLoc, render_max_age);
glUniform1ui(renderPreviewColormapLoc, preview_colormap);
if (render_max_age != 0 && options.automaton_options.rule.max_age != nullptr) {
glUniform1ui(renderGlobalMaxAgeLoc, options.automaton_options.rule.max_age->global_max());
glUniform1ui(renderGlobalMinimumMaxAgeLoc, options.automaton_options.rule.max_age->global_min());
}
glUniform1f(renderBlendStepLoc, static_cast<float>(step));
glUniform3f(renderDeadColorLoc, options.display_options.dead_color.v1, options.display_options.dead_color.v2, options.display_options.dead_color.v3);
glUniform1i(renderDeadColorIsCielabLoc, 1); // TODO: support multiple color spaces
if (std::holds_alternative<Color>(options.display_options.alive_color)) {
auto color = std::get<Color>(options.display_options.alive_color);
glUniform3f(renderLivingColorLoc, color.v1, color.v2, color.v3);
glUniform1i(renderLivingUseColormapLoc, 0);
} else {
auto colormap = std::get<ColorMapOption>(options.display_options.alive_color);
std::string colormap_name;
if (std::holds_alternative<std::string>(colormap.source)) {
colormap_name = std::get<std::string>(colormap.source);
} else {
colormap_name = "<custom>";
}
glUniform1i(renderLivingColormapLoc, 0);
glActiveTexture(GL_TEXTURE0 + 0);
glBindTexture(GL_TEXTURE_1D, colormap_textures.at(colormap_name));
glUniform1i(renderLivingUseColormapLoc, 1);
glUniform1i(renderLivingColormapInvertLoc, colormap.invert);
uint16_t scale;
bool is_max_age = false;
if (std::holds_alternative<uint16_t>(colormap.scale)) {
scale = std::get<uint16_t>(colormap.scale);
} else {
auto scale_enum = std::get<ColorMapScale>(colormap.scale);
if (options.automaton_options.rule.max_age == nullptr && (scale_enum == ColorMapScale::GLOBAL_MAX_AGE || scale_enum == ColorMapScale::MAX_AGE)) {
scale_enum = ColorMapScale::INHERENT;
}
switch (scale_enum) {
case ColorMapScale::INHERENT: {
ColorMapData data_source;
if (std::holds_alternative<ColorMapData>(colormap.source)) {
data_source = std::get<ColorMapData>(colormap.source);
} else {
data_source = colormaps.at(colormap_name);
}
scale = data_source.size();
} break;
case ColorMapScale::MAX_AGE:
is_max_age = true;
break;
case ColorMapScale::GLOBAL_MAX_AGE:
scale = options.automaton_options.rule.max_age->global_max();
break;
}
}
glUniform1ui(renderLivingColormapScaleLoc, scale);
glUniform1ui(renderLivingColormapScaleIsMaxAgeLoc, is_max_age);
}
glUniform1i(renderLivingColorIsCielabLoc, 1); // TODO: support multiple color spaces
glShaderStorageBlockBinding(renderProgram, renderBlockIndexFrom, from.bindingPoint);
glShaderStorageBlockBinding(renderProgram, renderBlockIndexTo, to.bindingPoint);
glEnableVertexAttribArray(0);
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, nullptr);
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
glDisableVertexAttribArray(0);
}
void Program::imguiDeadCells() {
auto color_rgb = options.display_options.dead_color.to_normalized_rgb();
float color[3] = {static_cast<float>(color_rgb[0]), static_cast<float>(color_rgb[1]), static_cast<float>(color_rgb[2])};
ImGui::ColorEdit3("##dead_cells_color_picker", color);
options.display_options.dead_color = Color::from_normalized_rgb(color[0], color[1], color[2], options.display_options.dead_color.colorspace);
}
void Program::imguiLivingCells() {
static ColorMapOption initial_colormap_value = ColorMapOption(colormaps.begin()->first);
static Color initial_color_value = Color();
bool is_colormap = std::holds_alternative<ColorMapOption>(options.display_options.alive_color);
if (is_colormap) {
initial_colormap_value = std::get<ColorMapOption>(options.display_options.alive_color);
} else {
initial_color_value = std::get<Color>(options.display_options.alive_color);
}
bool is_colormap_old = is_colormap;
int is_colormap_int = static_cast<int>(is_colormap);
ImGui::RadioButton("single color", &is_colormap_int, 0);
ImGui::SameLine();
ImGui::RadioButton("colormap", &is_colormap_int, 1);
is_colormap = static_cast<bool>(is_colormap_int);
if (is_colormap != is_colormap_old) {
if (is_colormap) {
options.display_options.alive_color = initial_colormap_value;
} else {
options.display_options.alive_color = initial_color_value;
}
}
if (is_colormap) {
auto colormapOpt = std::get<ColorMapOption>(options.display_options.alive_color);
std::string selected_colormap_name;
if (std::holds_alternative<std::string>(colormapOpt.source)) {
selected_colormap_name = std::get<std::string>(colormapOpt.source);
} else {
selected_colormap_name = "<custom>";
}
if (ImGui::BeginCombo("##living_cells_color_map", selected_colormap_name.c_str())) {
for (const auto &item : colormap_textures) {
bool is_selected = item.first == selected_colormap_name;
if (ImGui::Selectable(item.first.c_str(), is_selected)) {
selected_colormap_name = item.first;
}
if (is_selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
// TODO: custom colormap
std::variant<std::string, ColorMapData> colormap_source = selected_colormap_name;
bool invert_colormap = colormapOpt.invert;
ImGui::Checkbox("Invert colormap", &invert_colormap);
std::string colormap_scale_name;
uint16_t colormap_scale_num_value{};
if (std::holds_alternative<ColorMapScale>(colormapOpt.scale)) {
auto colormap_scale = std::get<ColorMapScale>(colormapOpt.scale);
switch (colormap_scale) {
case ColorMapScale::INHERENT:
colormap_scale_name = "inherent";
break;
case ColorMapScale::MAX_AGE:
colormap_scale_name = "max age";
break;
case ColorMapScale::GLOBAL_MAX_AGE:
colormap_scale_name = "global max age";
break;
}
} else {
colormap_scale_name = "number";
colormap_scale_num_value = std::get<uint16_t>(colormapOpt.scale);
}
std::vector<std::string> colormap_scale_names = {"inherent", "max age", "global max age", "number"};
if (ImGui::BeginCombo("Scale", colormap_scale_name.c_str())) {
for (const auto &item : colormap_scale_names) {
bool is_selected = item == colormap_scale_name;
if (ImGui::Selectable(item.c_str(), is_selected)) {
colormap_scale_name = item;
}
if (is_selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
if (colormap_scale_name == "number") {
ImGui::InputScalar("##living_cells_colormap_scale_number_value", ImGuiDataType_U16, &colormap_scale_num_value);
}
std::variant<ColorMapScale, uint16_t> colormap_scale;
if (colormap_scale_name == "inherent") {
colormap_scale = ColorMapScale::INHERENT;
} else if (colormap_scale_name == "max age") {
colormap_scale = ColorMapScale::MAX_AGE;
} else if (colormap_scale_name == "global max age") {
colormap_scale = ColorMapScale::GLOBAL_MAX_AGE;
} else {
colormap_scale = colormap_scale_num_value;
}
options.display_options.alive_color = ColorMapOption(colormap_source, invert_colormap, colormap_scale);
} else {
auto color_ = std::get<Color>(options.display_options.alive_color);
auto color_rgb = color_.to_normalized_rgb();
float color[3] = {static_cast<float>(color_rgb[0]), static_cast<float>(color_rgb[1]), static_cast<float>(color_rgb[2])};
ImGui::ColorEdit3("##living_cells_color_picker", color);
options.display_options.alive_color = Color::from_normalized_rgb(color[0], color[1], color[2], color_.colorspace);
}
}
void Program::imguiDisplayTab() {
ImGui::SeparatorText("Color for dead cells");
imguiDeadCells();
ImGui::SeparatorText("Color for living cells");
imguiLivingCells();
ImGui::SeparatorText("????");
static auto duration = options.display_options.generation_duration_ms;
ImGui::InputScalar("Time between generations (ms)", ImGuiDataType_U64, &duration, nullptr);
if (ImGui::IsItemDeactivatedAfterEdit()) {
options.display_options.generation_duration_ms = duration;
}
ImGui::Checkbox("Blend between generations", &options.display_options.blend);
}
bool Program::imguiBirthConditionTable(bool apply_changes, bool revert_changes) {
static bool has_changes = false;
static std::array<bool, 9> condition_selected = {};
if (!has_changes) {
condition_selected.fill(false);
for (const auto &item : options.automaton_options.rule.birth) {
condition_selected[item] = true;
}
}
if (ImGui::BeginTable("birth_condition_table", 9, ImGuiTableFlags_Borders | ImGuiTableFlags_NoHostExtendX | ImGuiTableFlags_SizingFixedSame)) {
for (int i = 0; i < 9; i++) {
ImGui::TableNextColumn();
if (ImGui::Selectable(std::format("{}", i).c_str(), condition_selected[i])) {
condition_selected[i] = !condition_selected[i];
has_changes = true;
}
}
ImGui::EndTable();
}
if (apply_changes) {
options.automaton_options.rule.birth.clear();
for (uint8_t i = 0; i < 9; i++) {
if (condition_selected[i] == true) {
options.automaton_options.rule.birth.push_back(i);
}
}
has_changes = false;
} else if (revert_changes) {
has_changes = false;
}
return has_changes;
}
bool Program::imguiSurviveConditionTable(bool apply_changes, bool revert_changes) {
static bool has_changes = false;
static std::array<bool, 9> condition_selected = {};
if (!has_changes) {
condition_selected.fill(false);
for (const auto &item : options.automaton_options.rule.survive) {
condition_selected[item] = true;
}
}
if (ImGui::BeginTable("survive_condition_table", 9, ImGuiTableFlags_Borders | ImGuiTableFlags_NoHostExtendX | ImGuiTableFlags_SizingFixedSame)) {
for (int i = 0; i < 9; i++) {
ImGui::TableNextColumn();
if (ImGui::Selectable(std::format("{}", i).c_str(), condition_selected[i])) {
condition_selected[i] = !condition_selected[i];
has_changes = true;
}
}
ImGui::EndTable();
}
if (apply_changes) {
options.automaton_options.rule.survive.clear();
for (uint8_t i = 0; i < 9; i++) {
if (condition_selected[i] == true) {
options.automaton_options.rule.survive.push_back(i);
}
}
has_changes = false;
} else if (revert_changes) {
has_changes = false;
}
return has_changes;
}
bool Program::imguiMaxAge(bool apply_changes, bool revert_changes) {
constexpr std::string_view type_name_none = "None";
constexpr std::string_view type_name_uniform = "Uniform";
constexpr std::string_view type_name_radial = "Radial";
constexpr std::string_view type_name_radial_fit = "Radial Fit";
constexpr std::string_view type_name_perlin_static = "Perlin Static";
constexpr std::string_view type_name_perlin_dynamic = "Perlin Dynamic";
std::array<std::string_view, 6> max_age_type_name_list = {type_name_none, type_name_uniform, type_name_radial, type_name_radial_fit, type_name_perlin_static, type_name_perlin_dynamic};
static bool has_changes = false;
static uint16_t value = 128;
static uint16_t radial_value_corner = 128;
static uint16_t perlin_max = 128;
static double perlin_scale = 0.5;
static uint16_t perlin_time_step = 128;
static double perlin_time_scale = 0.5;
static std::string max_age_type_name = std::string(type_name_none);
std::string max_age_type_name_old;
uint16_t value_old;
uint16_t radial_value_corner_old;
uint16_t perlin_max_old;
double perlin_scale_old;
uint16_t perlin_time_step_old;
double perlin_time_scale_old;
if (!has_changes) {
if (options.automaton_options.rule.max_age == nullptr) {
max_age_type_name = type_name_none;
} else if (auto static_provider = std::dynamic_pointer_cast<UniformMaxAgeProvider>(options.automaton_options.rule.max_age); static_provider != nullptr) {
max_age_type_name = type_name_uniform;
value = radial_value_corner = perlin_max = static_provider->value();
} else if (auto radial_provider = std::dynamic_pointer_cast<RadialMaxAgeProvider>(options.automaton_options.rule.max_age); radial_provider != nullptr) {
max_age_type_name = type_name_radial;
value = perlin_max = radial_provider->center();
radial_value_corner = radial_provider->corners();
} else if (auto radial_fit_provider = std::dynamic_pointer_cast<RadialFitMaxAgeProvider>(options.automaton_options.rule.max_age); radial_fit_provider != nullptr) {
max_age_type_name = type_name_radial_fit;
value = perlin_max = radial_fit_provider->center();
radial_value_corner = radial_fit_provider->corners();
} else if (auto perlin_static_provider = std::dynamic_pointer_cast<PerlinStaticMaxAgeProvider>(options.automaton_options.rule.max_age); perlin_static_provider != nullptr) {
max_age_type_name = type_name_perlin_static;
value = radial_value_corner = perlin_static_provider->min();
perlin_max = perlin_static_provider->max();
perlin_scale = perlin_static_provider->scale();
} else if (auto perlin_dynamic_provider = std::dynamic_pointer_cast<PerlinDynamicMaxAgeProvider>(options.automaton_options.rule.max_age); perlin_dynamic_provider != nullptr) {
max_age_type_name = type_name_perlin_dynamic;
value = radial_value_corner = perlin_dynamic_provider->min();
perlin_max = perlin_dynamic_provider->max();
perlin_time_step = perlin_dynamic_provider->time_step();
perlin_scale = perlin_dynamic_provider->scale();
perlin_time_scale = perlin_dynamic_provider->time_scale();
}
max_age_type_name_old = max_age_type_name;
value_old = value;
radial_value_corner_old = radial_value_corner;
perlin_max_old = perlin_max;
perlin_scale_old = perlin_scale;
perlin_time_step_old = perlin_time_step;
perlin_time_scale_old = perlin_time_scale;
}
if (ImGui::BeginCombo("##max_age_type", max_age_type_name.c_str())) {
for (const auto &item : max_age_type_name_list) {
bool is_selected = item == max_age_type_name;
if (ImGui::Selectable(std::string(item).c_str(), is_selected)) {
max_age_type_name = item;
}
if (is_selected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
if (max_age_type_name == type_name_uniform) {
ImGui::InputScalar("Value", ImGuiDataType_U16, &value);
}
if (max_age_type_name == type_name_radial || max_age_type_name == type_name_radial_fit) {
ImGui::InputScalar("Center value", ImGuiDataType_U16, &value);
ImGui::InputScalar("Corners value", ImGuiDataType_U16, &radial_value_corner);
}
if (max_age_type_name == type_name_perlin_static || max_age_type_name == type_name_perlin_dynamic) {
ImGui::InputScalar("Min value", ImGuiDataType_U16, &value);
ImGui::InputScalar("Max value", ImGuiDataType_U16, &perlin_max);
ImGui::InputScalar("Scale", ImGuiDataType_Double, &perlin_scale);
}
if (max_age_type_name == type_name_perlin_dynamic) {
ImGui::InputScalar("Time step", ImGuiDataType_U16, &perlin_time_step);
ImGui::InputScalar("Time scale", ImGuiDataType_Double, &perlin_time_scale);
}
has_changes = (max_age_type_name_old != max_age_type_name) ||
(value_old != value) ||
(radial_value_corner_old != radial_value_corner) ||
(perlin_max_old != perlin_max) ||
(perlin_scale_old != perlin_scale) ||
(perlin_time_step_old != perlin_time_step) ||
(perlin_time_scale_old != perlin_time_scale);
if (apply_changes) {
if (max_age_type_name == type_name_none) {
options.automaton_options.rule.max_age = nullptr;
} else if (max_age_type_name == type_name_uniform) {
options.automaton_options.rule.max_age = std::make_shared<UniformMaxAgeProvider>(value);
} else if (max_age_type_name == type_name_radial) {
options.automaton_options.rule.max_age = std::make_shared<RadialMaxAgeProvider>(value, radial_value_corner);
} else if (max_age_type_name == type_name_radial_fit) {
options.automaton_options.rule.max_age = std::make_shared<RadialFitMaxAgeProvider>(value, radial_value_corner);
} else if (max_age_type_name == type_name_perlin_static) {
options.automaton_options.rule.max_age = std::make_shared<PerlinStaticMaxAgeProvider>(perlin_scale, value, perlin_max);
} else if (max_age_type_name == type_name_perlin_dynamic) {
options.automaton_options.rule.max_age = std::make_shared<PerlinDynamicMaxAgeProvider>(perlin_scale, value, perlin_max, perlin_time_scale, perlin_time_step);
}
has_changes = false;
} else if (revert_changes) {
has_changes = false;
}
return has_changes;
}
bool Program::imguiStarve(bool apply_changes, bool revert_changes) {
static uint16_t starve_delay_val;
static bool starve_recover_val;
static bool has_changes = false;
if (!has_changes) {
starve_delay_val = options.automaton_options.rule.starve_delay;
starve_recover_val = options.automaton_options.rule.starve_recover;
}
ImGui::InputScalar("Delay", ImGuiDataType_U16, &starve_delay_val);
ImGui::Checkbox("Recover", &starve_recover_val);
has_changes = (starve_delay_val != options.automaton_options.rule.starve_delay) || (starve_recover_val != options.automaton_options.rule.starve_recover);
if (apply_changes) {
options.automaton_options.rule.starve_delay = starve_delay_val;
options.automaton_options.rule.starve_recover = starve_recover_val;
has_changes = false;
} else if (revert_changes) {
has_changes = false;
}
return has_changes;
}
void Program::imguiAutomatonTab() {
static bool has_changes;
bool apply_changes = false;
bool revert_changes = false;
// TODO: changing rules is only possible when starting/restarting
// there needs to be a restart button which opens the restart dialog
// and a command line flag to open it before starting
// the start/restart dialog has the automaton options and the display options
// and has initialiser options with the ability to draw
ImGui::BeginDisabled(!has_changes);
if (ImGui::BeginTable("apply_revert_buttons", 2, ImGuiTableFlags_SizingStretchSame)) {
ImGui::TableNextColumn();
if (ImGui::Button("Apply", ImVec2(-FLT_MIN, 0.0f))) {
apply_changes = true;
has_changes = false;
}
ImGui::TableNextColumn();
if (ImGui::Button("Revert", ImVec2(-FLT_MIN, 0.0f))) {
revert_changes = true;
has_changes = false;
}
ImGui::EndTable();
}
ImGui::EndDisabled();
has_changes = false;
ImGui::SeparatorText("Birth condition");
has_changes |= imguiBirthConditionTable(apply_changes, revert_changes);
ImGui::SeparatorText("Survive condition");
has_changes |= imguiSurviveConditionTable(apply_changes, revert_changes);
ImGui::SeparatorText("Max age");
// TODO: changes in the max age map don't seem to get applied
has_changes |= imguiMaxAge(apply_changes, revert_changes);
// TODO: initialiser and size
ImGui::SeparatorText("Starve");
has_changes |= imguiStarve(apply_changes, revert_changes);
}
void Program::imguiMainWindow() {
const ImGuiViewport *main_viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(ImVec2(main_viewport->WorkPos.x, main_viewport->WorkPos.y), ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(550, 680), ImGuiCond_Appearing);
if (ImGui::Begin("options", &show_ui, ImGuiWindowFlags_AlwaysAutoResize)) {
if (is_paused) {
if (ImGui::Button("Resume")) {
is_paused = false;
}
} else {
if (ImGui::Button("Pause")) {
is_paused = true;
}
}
if (ImGui::BeginTabBar("MyTabBar", ImGuiTabBarFlags_None)) {
if (ImGui::BeginTabItem("Display")) {
imguiDisplayTab();
ImGui::EndTabItem();
}
if (ImGui::BeginTabItem("Automaton")) {
imguiAutomatonTab();
ImGui::EndTabItem();
}
ImGui::EndTabBar();
}
}
ImGui::End();
}
void Program::imguiCloseWindow() {
ImVec2 center = ImGui::GetMainViewport()->GetCenter();
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
auto close_popup_name = "Close?";
if (ImGui::Begin("close_container", &show_close_popup, 0)) {
if (!ImGui::IsPopupOpen(close_popup_name)) {
ImGui::OpenPopup(close_popup_name);
}
ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (ImGui::BeginPopupModal(close_popup_name, &show_close_popup, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoDecoration)) {
ImGui::Text("Close?");
if (ImGui::Button("OK", ImVec2(120, 0))) {
glfwSetWindowShouldClose(window, 1);
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
show_close_popup = false;
}
if (ImGui::IsWindowFocused()) {
if (ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_close_popup = false;
}
}
ImGui::EndPopup();
}
}
ImGui::End();
}
void Program::renderUI() {
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
bool any_ui_visible = false;
if (show_ui) {
any_ui_visible = true;
ImGui::ShowDemoWindow(&show_ui);
imguiMainWindow();
}
if (show_close_popup) {
any_ui_visible = true;
imguiCloseWindow();
}
if (!any_ui_visible && is_fullscreen) {
ImGui::SetMouseCursor(ImGuiMouseCursor_None);
}
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
}
void Program::initializeMaxAge(BufferInfo buffer, uint64_t generation) const {
auto width = options.automaton_options.width;
auto height = options.automaton_options.height;
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer.bufferName);
auto *bufferData = static_cast<uint32_t *>(glMapBuffer(GL_SHADER_STORAGE_BUFFER, GL_WRITE_ONLY));
auto maxAgeProvider = options.automaton_options.rule.max_age;
if (maxAgeProvider != nullptr) {
maxAgeProvider->setup(width, height);
auto offset = width * height;
for (size_t ix = 0; ix < width; ++ix) {
for (size_t iy = 0; iy < height; ++iy) {
auto max_age = maxAgeProvider->getAt(ix, iy, generation);
bufferData[offset + ix + iy * width] = static_cast<size_t>(max_age);
}
}
}
glUnmapBuffer(GL_SHADER_STORAGE_BUFFER);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
}
void Program::initializeFirstBuffer(BufferInfo buffer) const {
auto width = options.automaton_options.width;
auto height = options.automaton_options.height;
glBindBuffer(GL_SHADER_STORAGE_BUFFER, buffer.bufferName);
auto *bufferData = static_cast<unsigned int *>(glMapBuffer(GL_SHADER_STORAGE_BUFFER, GL_WRITE_ONLY));
std::memset(bufferData, 0, width * height * sizeof(unsigned int) * 2);
auto initialiser = options.automaton_options.initialiser;
initialiser->setup(width, height);
for (size_t ix = 0; ix < width; ++ix) {
for (size_t iy = 0; iy < height; ++iy) {
bufferData[ix + iy * width] = initialiser->getCell(ix, iy) ? 1 : 0;
}
}
glUnmapBuffer(GL_SHADER_STORAGE_BUFFER);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
}
void Program::toggleFullscreen() {
if (is_fullscreen) {
glfwSetWindowMonitor(window, nullptr, window_pos_x, window_pos_y, window_width, window_height, GLFW_DONT_CARE);
is_fullscreen = false;
} else {
glfwGetWindowPos(window, &window_pos_x, &window_pos_y);
glfwGetWindowSize(window, &window_width, &window_height);
if (fullscreenMonitor == nullptr) {
fullscreenMonitor = glfwGetPrimaryMonitor();
}
auto mode = glfwGetVideoMode(fullscreenMonitor);
glfwSetWindowMonitor(window, fullscreenMonitor, 0, 0, mode->width, mode->height, GLFW_DONT_CARE);
is_fullscreen = true;
}
}
void Program::keyCallback(int key, int scancode, int action, int mods) {
if (ImGui::GetIO().WantCaptureKeyboard) {
return;
}
if (action == GLFW_PRESS) {
switch (key) {
case GLFW_KEY_Q:
show_close_popup = true;
break;
case GLFW_KEY_F:
toggleFullscreen();
break;
case GLFW_KEY_M:
render_max_age = (render_max_age + 1) % 3;
break;
case GLFW_KEY_C:
preview_colormap = !preview_colormap;
case GLFW_KEY_SPACE:
// TODO play/pause
break;
case GLFW_KEY_ENTER:
show_ui = true;
break;
default:
break;
}
}
}