mirror of
https://git.lynn.is/Gwen/pretty-automata.git
synced 2024-05-18 15:21:07 +02:00
1100 lines
42 KiB
C++
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;
|
|
}
|
|
}
|
|
}
|