#include #include #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 #include #include #include #include #include #include #include #include #include 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(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(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(options.display_options.alive_color)) { auto cmo = std::get(options.display_options.alive_color); if (std::holds_alternative(cmo.source)) { custom_colormap_data = std::get(cmo.source); } } auto colormaps_ = colormaps; colormaps_[""] = custom_colormap_data; auto num_colormaps = static_cast(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(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(sizeof(uint32_t) * options.automaton_options.width * options.automaton_options.height * 2); // 2x for data and max-age constexpr int numBuffers = 10; std::vector free_buffers(numBuffers); std::queue 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> 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>; 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 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(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(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>(generation_duration); auto dur_until_now = std::chrono::duration_cast>(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 &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(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(options.display_options.alive_color)) { auto color = std::get(options.display_options.alive_color); glUniform3f(renderLivingColorLoc, color.v1, color.v2, color.v3); glUniform1i(renderLivingUseColormapLoc, 0); } else { auto colormap = std::get(options.display_options.alive_color); std::string colormap_name; if (std::holds_alternative(colormap.source)) { colormap_name = std::get(colormap.source); } else { colormap_name = ""; } 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(colormap.scale)) { scale = std::get(colormap.scale); } else { auto scale_enum = std::get(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(colormap.source)) { data_source = std::get(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(color_rgb[0]), static_cast(color_rgb[1]), static_cast(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(options.display_options.alive_color); if (is_colormap) { initial_colormap_value = std::get(options.display_options.alive_color); } else { initial_color_value = std::get(options.display_options.alive_color); } bool is_colormap_old = is_colormap; int is_colormap_int = static_cast(is_colormap); ImGui::RadioButton("single color", &is_colormap_int, 0); ImGui::SameLine(); ImGui::RadioButton("colormap", &is_colormap_int, 1); is_colormap = static_cast(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(options.display_options.alive_color); std::string selected_colormap_name; if (std::holds_alternative(colormapOpt.source)) { selected_colormap_name = std::get(colormapOpt.source); } else { selected_colormap_name = ""; } 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 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(colormapOpt.scale)) { auto colormap_scale = std::get(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(colormapOpt.scale); } std::vector 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 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(options.display_options.alive_color); auto color_rgb = color_.to_normalized_rgb(); float color[3] = {static_cast(color_rgb[0]), static_cast(color_rgb[1]), static_cast(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 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 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 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(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(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(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(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(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(value); } else if (max_age_type_name == type_name_radial) { options.automaton_options.rule.max_age = std::make_shared(value, radial_value_corner); } else if (max_age_type_name == type_name_radial_fit) { options.automaton_options.rule.max_age = std::make_shared(value, radial_value_corner); } else if (max_age_type_name == type_name_perlin_static) { options.automaton_options.rule.max_age = std::make_shared(perlin_scale, value, perlin_max); } else if (max_age_type_name == type_name_perlin_dynamic) { options.automaton_options.rule.max_age = std::make_shared(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(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(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(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; } } }