#include "pch.h" #include "log.h" #include "app.h" #include "node_icon.h" #include "node_dialog_open.h" #include "node_progress_bar.h" #include "mp4enc.h" #include "app_core/app_status.h" #include "app_core/canvas_tool_ui.h" #include "app_core/document_recording.h" #include "app_core/document_route.h" #include "app_core/document_session.h" #include "platform_api/platform_services.h" #include "renderer_gl/opengl_capabilities.h" #ifdef __APPLE__ #include #include "objc_utils.h" #endif #include "settings.h" App* App::I = nullptr; // singleton std::deque App::render_tasklist; std::mutex App::render_task_mutex; std::condition_variable App::render_cv; namespace { [[nodiscard]] const char* query_opengl_string(std::uint32_t name) noexcept { return reinterpret_cast(glGetString(static_cast(name))); } void enable_opengl_state(std::uint32_t state) noexcept { glEnable(static_cast(state)); } pp::app::CanvasToolMode canvas_tool_mode_from_canvas_mode(kCanvasMode mode) noexcept { switch (mode) { case kCanvasMode::Draw: return pp::app::CanvasToolMode::draw; case kCanvasMode::Erase: return pp::app::CanvasToolMode::erase; case kCanvasMode::Line: return pp::app::CanvasToolMode::line; case kCanvasMode::Camera: return pp::app::CanvasToolMode::camera; case kCanvasMode::Grid: return pp::app::CanvasToolMode::grid; case kCanvasMode::Copy: return pp::app::CanvasToolMode::copy; case kCanvasMode::Cut: return pp::app::CanvasToolMode::cut; case kCanvasMode::Fill: return pp::app::CanvasToolMode::fill; case kCanvasMode::MaskFree: return pp::app::CanvasToolMode::mask_free; case kCanvasMode::MaskLine: return pp::app::CanvasToolMode::mask_line; case kCanvasMode::FloodFill: return pp::app::CanvasToolMode::flood_fill; default: return pp::app::CanvasToolMode::draw; } } void disable_opengl_state(std::uint32_t state) noexcept { glDisable(static_cast(state)); } void set_opengl_blend_func(std::uint32_t source_factor, std::uint32_t destination_factor) noexcept { glBlendFunc(static_cast(source_factor), static_cast(destination_factor)); } void set_opengl_blend_equation_separate(std::uint32_t color_equation, std::uint32_t alpha_equation) noexcept { glBlendEquationSeparate(static_cast(color_equation), static_cast(alpha_equation)); } void clear_opengl_color(float r, float g, float b, float a) noexcept { glClearColor(r, g, b, a); } void clear_opengl_buffers(std::uint32_t mask) noexcept { glClear(static_cast(mask)); } void set_opengl_viewport(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept { glViewport(static_cast(x), static_cast(y), static_cast(width), static_cast(height)); } void set_opengl_scissor(std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height) noexcept { glScissor(static_cast(x), static_cast(y), static_cast(width), static_cast(height)); } void apply_app_viewport(pp::renderer::gl::OpenGlViewportRect viewport) { const auto status = pp::renderer::gl::apply_opengl_viewport( viewport, pp::renderer::gl::OpenGlViewportDispatch { .viewport = set_opengl_viewport, }); if (!status.ok()) LOG("OpenGL viewport failed: %s", status.message); } void apply_app_scissor(pp::renderer::gl::OpenGlScissorRect scissor) { const auto status = pp::renderer::gl::apply_opengl_scissor_rect( scissor, pp::renderer::gl::OpenGlScissorDispatch { .enable = enable_opengl_state, .disable = disable_opengl_state, .scissor = set_opengl_scissor, }); if (!status.ok()) LOG("OpenGL scissor failed: %s", status.message); } void apply_app_scissor_test(bool enabled) { const auto status = pp::renderer::gl::apply_opengl_scissor_test( enabled, pp::renderer::gl::OpenGlScissorTestDispatch { .enable = enable_opengl_state, .disable = disable_opengl_state, }); if (!status.ok()) LOG("OpenGL scissor test failed: %s", status.message); } [[nodiscard]] GLint rgba8_internal_format() noexcept { return static_cast(pp::renderer::gl::rgba8_internal_format()); } [[nodiscard]] GLenum linear_texture_filter() noexcept { return static_cast(pp::renderer::gl::linear_texture_filter()); } [[nodiscard]] GLenum nearest_texture_filter() noexcept { return static_cast(pp::renderer::gl::nearest_texture_filter()); } [[nodiscard]] GLenum repeat_texture_wrap() noexcept { return static_cast(pp::renderer::gl::repeat_texture_wrap()); } [[nodiscard]] GLenum framebuffer_target() noexcept { return static_cast(pp::renderer::gl::framebuffer_target()); } [[nodiscard]] GLuint default_framebuffer_id() noexcept { return static_cast(pp::renderer::gl::default_framebuffer_id()); } } std::thread App::render_thread; std::thread::id App::render_thread_id; bool App::render_running = false; std::deque App::ui_tasklist; std::mutex App::ui_task_mutex; std::condition_variable App::ui_cv; std::thread App::ui_thread; std::thread::id App::ui_thread_id; bool App::ui_running = false; void App::create() { width = 1920/2; height = 1080/2; } void App::open_document(std::string path) { const auto route = pp::app::classify_document_open_path(path); if (!route) return; const bool has_unsaved_project = route.value().kind == pp::app::DocumentOpenKind::open_project && Canvas::I->m_unsaved; const auto open_plan = pp::app::plan_document_open(route.value().kind, has_unsaved_project); if (open_plan == pp::app::DocumentOpenPlanAction::prompt_import_abr) { auto mb = message_box("Import ABR", "Would you like to import the brushes?", true); mb->on_submit = [this, path] (Node* target) { std::thread(&NodePanelBrushPreset::import_abr, presets, path).detach(); target->destroy(); }; } else if (open_plan == pp::app::DocumentOpenPlanAction::prompt_import_ppbr) { auto mb = message_box("Import PPBR", "Would you like to import the brushes?", true); mb->on_submit = [this, path] (Node* target) { std::thread(&NodePanelBrushPreset::import_ppbr, presets, path).detach(); target->destroy(); }; } else { const std::string base = route.value().directory; const std::string name = route.value().name; auto open_action = [this, path, base, name] { doc_name = name; doc_dir = base; doc_path = path; canvas->reset_camera(); layers->clear(); canvas->m_canvas->project_open(path, [this](bool success){ // on complete if (success) { title_update(); for (int layer_index = 0; layer_index < canvas->m_canvas->m_layers.size(); layer_index++) { auto l = layers->add_layer(canvas->m_canvas->m_layers[layer_index]->m_name.c_str(), false); l->m_visibility->set_value(canvas->m_canvas->m_layers[layer_index]->m_visible); } } else { message_box("Open Document Error", "There was an error opening the document.\n" "It may be inaccessible or corrupted."); } }); ActionManager::clear(); }; if (open_plan == pp::app::DocumentOpenPlanAction::open_project_now) { open_action(); } else { auto mb = message_box("Unsaved document", "Do you want to close the unsaved document before opening the file?", true); mb->on_submit = [this, open_action] (Node* target) { open_action(); target->destroy(); }; } } } bool App::request_close() { static bool dialog_already_opened = false; const auto close_decision = pp::app::plan_close_request( Canvas::I->m_unsaved, dialog_already_opened); if (close_decision == pp::app::CloseRequestDecision::close_now) return true; if (close_decision == pp::app::CloseRequestDecision::show_unsaved_prompt) { auto* m = layout[main_id]->add_child(); m->m_title->set_text("Unsaved document"); m->m_message->set_text("Do you want to close without saving?"); m->btn_ok->m_text->set_text("Yes"); m->btn_ok->on_click = [this](Node*) { request_app_close(); Canvas::I->m_unsaved = false; }; m->btn_cancel->m_text->set_text("No"); m->btn_cancel->on_click = [this,m](Node*) { m->destroy(); dialog_already_opened = false; }; dialog_already_opened = true; } return false; } void App::clear() { const auto status = pp::renderer::gl::clear_panopainter_default_target( pp::renderer::gl::OpenGlClearDispatch { .clear_color = clear_opengl_color, .clear = clear_opengl_buffers, }); if (!status.ok()) LOG("OpenGL clear failed: %s", status.message); } void App::initAssets() { LOG("initializing assets"); FontManager::init(); LOG("initializing assets create sampler"); sampler.create(nearest_texture_filter()); sampler_stencil.create(linear_texture_filter(), repeat_texture_wrap()); sampler_linear.create(linear_texture_filter()); m_face_plane.create<1>(2, 2); sphere.create<8, 8>(1); LOG("initializing assets load uvs texture"); LOG("initializing assets completed"); } void App::initLog() { const auto paths = prepare_storage_paths(); if (!paths.data_path.empty()) data_path = paths.data_path; if (!paths.recording_path.empty()) rec_path = paths.recording_path; if (!paths.temporary_path.empty()) tmp_path = paths.temporary_path; // TODO: save this path somewhere in the settings, don't overwrite every start work_path = paths.work_path.empty() ? data_path : paths.work_path; //LogRemote::I.start(); LogRemote::I.file_init(); LOG("%s", g_version); LOG("load preferences"); if (!Settings::load()) LOG("load preferences failed"); } #if WITH_CURL int progress_callback_download(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { std::function progress = *(std::function*)clientp; progress((float)dlnow / (float)dltotal); return 0; } int progress_callback_upload(void *clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { std::function progress = *(std::function*)clientp; progress((float)ulnow / (float)ultotal); return 0; } #endif //CURL void App::download(std::string url, std::string dest_filepath, std::function progress) { #if WITH_CURL CURL *curl = curl_easy_init(); if (curl) { FILE* fp = fopen(dest_filepath.c_str(), "wb"); LOG("download %s to %s", url.c_str(), dest_filepath.c_str()); curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_data_write); #ifdef __ANDROID__ curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); #endif if (progress) { curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback_download); curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &progress); curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); } auto err = curl_easy_perform(curl); curl_easy_cleanup(curl); fclose(fp); } #endif //CURL } bool App::check_license() { return true; // TODO: for distribuiton only #if WITH_CURL CURL *curl = curl_easy_init(); if (curl) { std::string url = "https://panopainter.com/license/7565D057-ACBE-4721-9A4E-F342D3DDB7D8.php"; //std::string url = "https://panopainter.com/license/E8EDC2FE-E1BD-4AB1-91BD-FCCD926739BD.php"; // wacom //std::string url = "https://panopainter.com/license/A744FBA9-BC6C-43C8-BD24-0CCE24B3D985.php"; // others std::string ret; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &ret); //curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_data_handler); curl_easy_setopt(curl, CURLOPT_TIMEOUT, 2L); #ifdef __ANDROID__ curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); #endif auto err = curl_easy_perform(curl); curl_easy_cleanup(curl); LOG("License check: %s", ret.c_str()); if (err == CURLcode::CURLE_OK && ret == "success") return true; } #endif //CURL return false; } void App::upload(std::string filename, std::string name, std::function progress) { #if WITH_CURL CURL *curl; struct curl_httppost *formpost = NULL; struct curl_httppost *lastptr = NULL; //curl_global_init(CURL_GLOBAL_ALL); curl_formadd(&formpost, &lastptr, CURLFORM_COPYNAME, "fileToUpload", CURLFORM_FILE, filename.c_str(), CURLFORM_END); curl = curl_easy_init(); std::string res; if (curl) { std::string url = "https://panopainter.com/cloud/cloud-upl.php?name=" + name; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_HTTPPOST, formpost); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &res); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_data_handler); #ifdef __ANDROID__ curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); #endif if (progress) { curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback_upload); curl_easy_setopt(curl, CURLOPT_XFERINFODATA, &progress); curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0); } auto err = curl_easy_perform(curl); std::cout << "\n\nUPLOAD RESULT\n" << res << "\n\n\n"; curl_easy_cleanup(curl); } #endif //CURL } void App::init() { LOG("Screen Resolution: %dx%d", (int)width, (int)height); render_task([] { App::I->install_render_debug_callback(); const auto runtime_info_result = pp::renderer::gl::query_opengl_runtime_info( pp::renderer::gl::OpenGlRuntimeInfoDispatch { .get_string = query_opengl_string, }); if (runtime_info_result.ok()) { const auto& runtime_info = runtime_info_result.value(); LOG("GL version: %s", runtime_info.version); LOG("GL vendor: %s", runtime_info.vendor); LOG("GL renderer: %s", runtime_info.renderer); LOG("GLSL version: %s", runtime_info.shading_language_version); } else { LOG("OpenGL runtime info failed: %s", runtime_info_result.status().message); } //GLint n_exts; //glGetIntegerv(GL_NUM_EXTENSIONS, &n_exts); //for (int i = 0; i < n_exts; i++) //{ // std::string ext = (const char*)glGetStringi(GL_EXTENSIONS, i); // //if (ext.find("debug") != std::string::npos) // { // LOG("%s", glGetStringi(GL_EXTENSIONS, i)); // } //} App::I->apply_render_platform_hints(); const auto startup_state_status = pp::renderer::gl::apply_panopainter_initial_state( pp::renderer::gl::OpenGlStateDispatch { .enable = enable_opengl_state, .disable = disable_opengl_state, .blend_func = set_opengl_blend_func, .blend_equation_separate = set_opengl_blend_equation_separate, }); if (!startup_state_status.ok()) LOG("OpenGL startup state failed: %s", startup_state_status.message); }); int run_counter = Settings::value("run_counter") + 1; Settings::set("run_counter", Serializer::Integer(run_counter)); LOG("run_counter %d", run_counter); if (!Settings::save()) LOG("save preferences failed"); initShaders(); initAssets(); initLayout(); title_update(); uirtt.create(width, height, -1, rgba8_internal_format(), true); if (Settings::value_or("auto-timelapse", true)) rec_start(); Settings::value("vr-controllers-enabled", vr_controllers_enabled); if (!check_license()) { message_box("License", "Could not validate this license, running in demo mode."); } } void App::async_start() { acquire_render_context(); } void App::async_redraw() { redraw = true; ui_cv.notify_all(); } void App::async_end() { release_render_context(); } void App::async_swap() { present_render_context(); } bool App::update_ui_observer(Node *n) { if (n && n->m_display) { auto box = n->m_clip_uncut; Node* p = n->m_parent; while (p) { float pt = YGNodeLayoutGetPadding(p->y_node, YGEdgeTop); float pr = YGNodeLayoutGetPadding(p->y_node, YGEdgeRight); float pb = YGNodeLayoutGetPadding(p->y_node, YGEdgeBottom); float pl = YGNodeLayoutGetPadding(p->y_node, YGEdgeLeft); glm::vec2 off_p(pl, pt); glm::vec2 off_s(pr, pb); //glm::vec2 parent_offset = p->m_parent ? -p->m_parent->m_pos_offset_childred : glm::vec2(0.f); glm::vec4 pclip = { xy(p->m_clip_uncut) + off_p, zw(p->m_clip_uncut) - off_s - off_p/* + parent_offset*/ }; box = rect_intersection(box, pclip); p = p->m_parent; } //auto box = n->m_clip; //glm::ivec4 c = glm::vec4((int)box.x - 1, (int)(height / zoom - box.y - box.w) - 1, (int)box.z + 2, (int)box.w + 2) * zoom; glm::vec2 parent_offset = n->m_parent ? n->m_parent->m_pos_offset_childred : glm::vec2(0.f); if (box.z <= 0 || box.w <= 0) { if (n->m_on_screen) { if (dynamic_cast(n)) p = p; n->handle_on_screen(true, false); n->m_on_screen = false; } return false; } if (!n->m_on_screen) { n->handle_on_screen(false, true); n->m_on_screen = true; } glm::ivec4 c = glm::vec4(box.x - 1, (height / zoom - box.y - box.w - 1), box.z + 2, box.w + 2) * zoom; apply_app_scissor(pp::renderer::gl::OpenGlScissorRect { .enabled = 1U, .x = static_cast(floorf(c.x + off_x)), .y = static_cast(floorf(c.y + off_y)), .width = static_cast(ceilf(c.z)), .height = static_cast(ceilf(c.w)), }); n->draw(); return true; } return false; } void App::draw(float dt) { // update offscreen stuff if (canvas && canvas->m_canvas) canvas->m_canvas->stroke_draw(); auto observer = std::bind(&App::update_ui_observer, this, std::placeholders::_1); if (vr_active && ui_visible) { uirtt.bindFramebuffer(); uirtt.clear(); apply_app_viewport(pp::renderer::gl::OpenGlViewportRect { .width = static_cast(uirtt.getWidth()), .height = static_cast(uirtt.getHeight()), }); apply_app_scissor_test(true); for (int i = 1; i < layout[main_id]->m_children.size(); i++) layout[main_id]->m_children[i]->watch(observer); for (int i = 0; layout_designer.get(main_id) && i < layout_designer[main_id]->m_children.size(); i++) layout_designer[main_id]->m_children[i]->watch(observer); //msgbox->watch(observer); apply_app_scissor_test(false); uirtt.unbindFramebuffer(); } if (!vr_only) { bind_main_render_target(); apply_app_viewport(pp::renderer::gl::OpenGlViewportRect { .x = static_cast(off_x), .y = static_cast(off_y), .width = static_cast(width), .height = static_cast(height), }); apply_app_scissor_test(true); for (int i = 0; i < layout[main_id]->m_children.size(); i++) layout[main_id]->m_children[i]->watch(observer); for (int i = 0; layout_designer.get(main_id) && i < layout_designer[main_id]->m_children.size(); i++) layout_designer[main_id]->m_children[i]->watch(observer); //msgbox->watch(observer); apply_app_scissor_test(false); } redraw = false; } void App::update(float dt) { static std::mutex mutex; // avoid multiple threads to update the scene //std::lock_guard lock(mutex); if (!(redraw || animate)) return; if (auto* main = layout[main_id]) main->update(width, height, zoom); if (auto* main = layout_designer[main_id]) main->update(width, height, zoom); { auto mode = Canvas::I->m_current_mode; CanvasModePen* pm = (CanvasModePen*)canvas->m_canvas->modes[(int)kCanvasMode::Draw][0]; const auto toolbar = pp::app::plan_canvas_tool_button_state( canvas_tool_mode_from_canvas_mode(mode), pm && pm->m_picking, canvas->m_canvas->m_touch_lock); layout[main_id]->find("btn-pick")->set_active(toolbar.pick_active); layout[main_id]->find("btn-touchlock")->set_active(toolbar.touch_lock_active); layout[main_id]->find("btn-pen")->set_active(toolbar.pen_active); layout[main_id]->find("btn-erase")->set_active(toolbar.erase_active); layout[main_id]->find("btn-cam")->set_active(toolbar.camera_active); layout[main_id]->find("btn-line")->set_active(toolbar.line_active); layout[main_id]->find("btn-grid")->set_active(toolbar.grid_active); layout[main_id]->find("btn-copy")->set_active(toolbar.copy_active); layout[main_id]->find("btn-cut")->set_active(toolbar.cut_active); layout[main_id]->find("btn-mask-free")->set_active(toolbar.mask_free_active); layout[main_id]->find("btn-mask-line")->set_active(toolbar.mask_line_active); layout[main_id]->find("btn-bucket")->set_active(toolbar.flood_fill_active); } } void App::terminate() { LOG("App::terminate"); ui_save(); NodeStrokePreview::terminate_renderer(); rec_stop(); TextureManager::invalidate(); ShaderManager::invalidate(); layout.unload(); layout_designer.unload(); uirtt.destroy(); m_face_plane.destroy(); layers.reset(); color.reset(); stroke.reset(); grid.reset(); presets.reset(); floating_presets.reset(); floating_color.reset(); floating_layers.reset(); floating_picker.reset(); quick_mode_state.clear(); } void App::update_memory_usage(size_t bytes) { if (auto txt = layout[main_id]->find("txt-memory")) { const auto label = pp::app::make_history_memory_label(bytes); txt->set_text(label.c_str()); } } void App::update_rec_frames() { if (auto txt = layout[main_id]->find("txt-rec")) { const auto label = pp::app::make_recording_frame_label( rec_running, Canvas::I->m_encoder != nullptr, Canvas::I->m_encoder ? Canvas::I->m_encoder->frames_count() : 0); txt->set_text(label.text.c_str()); } } int App::res_from_index(int i) { const auto resolution = pp::app::display_resolution_from_index(i); return resolution ? resolution.value() : pp::app::document_resolution_values.front(); } int App::res_to_index(int res) { const auto index = pp::app::document_resolution_to_index(res); return index ? static_cast(index.value()) : static_cast(pp::app::document_resolution_values.size()); } std::string App::res_to_string(int res) { const auto label = pp::app::document_resolution_label(res); return label ? std::string(label.value()) : std::string("unknown"); } void App::renderdoc_frame_start() { begin_render_capture_frame(); } void App::renderdoc_frame_end() { end_render_capture_frame(); } void App::rec_clear() { const auto plan = pp::app::plan_recording_clear( rec_running, platform_deletes_recorded_files_on_clear() ); if (plan.stop_running_recording) rec_stop(); if (plan.delete_recorded_files) clear_platform_recorded_files(rec_path); rec_count = plan.frame_count_after_clear; update_rec_frames(); } void App::rec_start() { const auto plan = pp::app::plan_recording_start(rec_running); switch (plan) { case pp::app::RecordingStartAction::start_thread: break; case pp::app::RecordingStartAction::no_op_already_running: return; } update_rec_frames(); rec_thread = std::thread(&App::rec_loop, this); } void App::rec_stop() { const auto plan = pp::app::plan_recording_stop(rec_running); switch (plan) { case pp::app::RecordingStopAction::stop_thread: break; case pp::app::RecordingStopAction::no_op_not_running: return; } rec_running = false; rec_cv.notify_all(); if (rec_thread.joinable()) rec_thread.join(); update_rec_frames(); } void App::rec_export(std::string path) { const auto plan = pp::app::plan_recording_export(static_cast(rec_count)); auto pb = layout[main_id]->add_child(); pb->m_progress->SetWidthP(0); pb->m_title->set_text("Exporting MP4 movie"); pb->m_total = plan.progress_total; pb->m_count = 0; /* #if defined(__IOS__) || defined(__OSX__) export_mp4(rec_path, width, height, rec_count, ^(float) { pb->increment(); }); #endif */ Canvas::I->m_encoder->write_mp4(path); pb->destroy(); } void App::rec_loop() { BT_SetTerminate(); rec_running = true; while(rec_running) { std::unique_lock lock(rec_mutex); rec_cv.wait(lock/*, [this] { return !(rec_frames.empty() && rec_running); }*/); if (!rec_running) break; if (Canvas::I->m_encoder) { canvas->m_canvas->m_dirty_stroke = false; PBO equirect = Canvas::I->m_layers_merge.gen_equirect_pbo( Canvas::I->m_encoder->frame_size()); std::this_thread::yield(); ImageRef img; img.create(equirect.width, equirect.height, equirect.map()); Canvas::I->m_encoder->encode(img); equirect.unmap(); LOG("rec frame encoded"); update_rec_frames(); } } } void App::render_thread_tick() { static uint32_t count = 0; render_thread_id = std::this_thread::get_id(); render_running = true; std::deque working_list; // move the task list locally to free the queue for other threads { std::unique_lock lock(render_task_mutex); if (render_tasklist.empty()) return; working_list = std::move(render_tasklist); } // execute the tasks if (!working_list.empty()) { async_start(); while (!working_list.empty()) { //LOG("render task %d", count); //LOG("task %s", working_list.front().name.c_str()); count++; working_list.front()(); working_list.pop_front(); } async_end(); } } void App::render_thread_main() { BT_SetTerminate(); uint32_t count = 0; render_thread_id = std::this_thread::get_id(); render_running = true; while (render_running) { std::deque working_list; // move the task list locally to free the queue for other threads { std::unique_lock lock(render_task_mutex); render_cv.wait(lock, [this] { return render_tasklist.empty() && render_running ? false : true; }); working_list = std::move(render_tasklist); } // execute the tasks if (!working_list.empty()) { async_start(); while (!working_list.empty()) { //LOG("render task %d", count); //LOG("task %s", working_list.front().name.c_str()); count++; working_list.front()(); working_list.pop_front(); } async_end(); } } } void App::ui_thread_tick() { ui_thread_id = std::this_thread::get_id(); ui_running = true; std::deque working_list; // move the task list locally to free the queue for other threads { std::unique_lock lock(ui_task_mutex); working_list = std::move(ui_tasklist); } // execute the tasks if (!working_list.empty()) { while (!working_list.empty()) { //LOG("ui task %d", count); working_list.front()(); working_list.pop_front(); } } tick(0); if (redraw) { update(0); render_task([this] { bind_default_render_target(); clear(); draw(0); async_swap(); }); } } void App::ui_thread_main() { BT_SetTerminate(); uint32_t count = 0; ui_thread_id = std::this_thread::get_id(); ui_running = true; attach_ui_thread(); LOG("ui thread init()"); init(); auto t_start = std::chrono::high_resolution_clock::now(); float t_frame = 0; float t_fps_counter = 0; float t_reloader = 0; int rendered_frames = 0; while (ui_running) { std::deque working_list; // move the task list locally to free the queue for other threads { std::unique_lock lock(ui_task_mutex); ui_cv.wait_for(lock, std::chrono::milliseconds(idle_ms), [this] { return ui_tasklist.empty() && ui_running ? false : true; }); working_list = std::move(ui_tasklist); } // execute the tasks if (!working_list.empty()) { while (!working_list.empty()) { //LOG("ui task %d", count); count++; working_list.front()(); working_list.pop_front(); } } auto t_now = std::chrono::high_resolution_clock::now(); float dt = std::chrono::duration(t_now - t_start).count(); t_start = t_now; update_platform_frame(dt); // increment timers t_frame += dt; t_fps_counter += dt; if (t_fps_counter > 1.f) { report_rendered_frames(rendered_frames); t_fps_counter = 0; rendered_frames = 0; } if (platform_enables_live_asset_reloading()) { t_reloader += dt; if (t_reloader > 1.0) { t_reloader = 0; if (ShaderManager::reload()) { stroke->update_controls(); redraw = true; } if (layout.reload()) redraw = true; if (layout_designer.reload()) redraw = true; } } tick(dt); if (redraw) { update(t_frame); render_task([this, t_frame] { bind_default_render_target(); clear(); draw(t_frame); async_swap(); }); t_frame = 0; rendered_frames++; } } detach_ui_thread(); } void App::render_thread_start() { render_thread = std::thread(&App::render_thread_main, this); render_running = true; } void App::render_thread_stop() { render_running = false; render_cv.notify_all(); if (render_thread.joinable()) render_thread.join(); } void App::ui_thread_start() { ui_thread = std::thread(&App::ui_thread_main, this); ui_running = true; } void App::ui_thread_stop() { ui_running = false; ui_cv.notify_all(); if (ui_thread.joinable()) ui_thread.join(); }