#include "pch.h" #include "log.h" #include "app.h" #include "node_icon.h" #include "node_dialog_open.h" #include "node_progress_bar.h" #ifdef __APPLE__ #include #endif #include "settings.h" #ifdef __ANDROID__ void android_async_lock(struct engine* engine); void android_async_swap(struct engine* engine); void android_async_unlock(struct engine* engine); #elif _WIN32 bool async_lock_try(); void async_lock(); void win32_async_swap(); void async_unlock(); void destroy_window(); #endif App App::I; // singleton void App::create() { width = 1920/2; height = 1080/2; } void App::open_document(std::string path) { std::regex r(R"((.*)[\\/]([^\\/]+)\.(\w+)$)"); std::smatch m; if (!std::regex_search(path, m, r)) return; std::string ext = m[3].str(); if (str_iequals(m[3].str(), "abr")) { std::thread(&NodePanelStroke::import_abr, stroke, path).detach(); } else { doc_name = m[2].str(); doc_dir = m[1].str(); 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(); } } bool App::request_close() { static bool dialog_already_opened = false; if (!Canvas::I->m_unsaved) return true; if (!dialog_already_opened) { 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*) { #ifdef _WIN32 destroy_window(); //PostQuitMessage(0); #endif #ifdef __OSX__ [osx_view close]; #endif 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() { glClearColor(.1f, .1f, .1f, 1.f); glClear(GL_COLOR_BUFFER_BIT); } void App::initAssets() { LOG("initializing assets"); FontManager::init(); LOG("initializing assets loading fonts"); FontManager::load(kFont::Arial_11, "data/fonts/Roboto-Regular.ttf", 17); FontManager::load(kFont::Arial_30, "data/fonts/Roboto-Regular.ttf", 30); LOG("initializing assets create sampler"); sampler.create(GL_NEAREST); sampler_stencil.create(GL_LINEAR, GL_REPEAT); sampler_linear.create(GL_LINEAR); 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() { #if defined(__IOS__) NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString* docpath = (NSString*)[paths objectAtIndex:0]; data_path = [docpath cStringUsingEncoding:NSASCIIStringEncoding]; NSError* err = nil; NSString* recpath = [docpath stringByAppendingString:@"/rec"]; rec_path = [recpath cStringUsingEncoding:NSASCIIStringEncoding]; if (![[NSFileManager defaultManager] createDirectoryAtPath:recpath withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating rec path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } // brushes if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/brushes"] withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating brushes path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/brushes/thumbs"] withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating brushes thumbs path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } // patterns if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/patterns"] withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating patterns path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/patterns/thumbs"] withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating patterns thumbs path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } // settings if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/settings"] withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating settings path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } #elif defined(__OSX__) NSArray* paths = NSSearchPathForDirectoriesInDomains(NSPicturesDirectory, NSUserDomainMask, YES); NSString* docpath = [(NSString*)[paths objectAtIndex:0] stringByAppendingString:@"/PanoPainter"]; NSError* err = nil; if (![[NSFileManager defaultManager] createDirectoryAtPath:docpath withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating rec path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } data_path = [docpath cStringUsingEncoding:NSASCIIStringEncoding]; NSString* recpath = [docpath stringByAppendingString:@"/rec"]; rec_path = [recpath cStringUsingEncoding:NSASCIIStringEncoding]; if (![[NSFileManager defaultManager] createDirectoryAtPath:recpath withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating rec path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } // brushes if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/brushes"] withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating brushes path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/brushes/thumbs"] withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating brushes thumbs path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } // textures if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/patterns"] withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating patterns path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/patterns/thumbs"] withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating patterns thumbs path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } // settings if (![[NSFileManager defaultManager] createDirectoryAtPath:[docpath stringByAppendingString:@"/settings"] withIntermediateDirectories:YES attributes:nil error:&err]) { LOG("error creating settings path: %s", [[err localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } #elif defined(_WIN32) //CHAR my_documents[MAX_PATH]; //HRESULT result = SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, my_documents); //HMODULE hModule = GetModuleHandle(NULL); //CHAR path[MAX_PATH]; //GetModuleFileNameA(hModule, path, MAX_PATH); //CHAR out_drive[MAX_PATH]; //CHAR out_path[MAX_PATH]; //_splitpath(path, out_drive, out_path, nullptr, nullptr); //sprintf_s(path, "%s%s", out_drive, out_path); //data_path = path; CHAR my_documents[MAX_PATH]; HRESULT result = SHGetFolderPathA(NULL, CSIDL_PERSONAL, NULL, SHGFP_TYPE_CURRENT, my_documents); if (SUCCEEDED(result)) { std::string path = std::string(my_documents) + "\\PanoPainter"; if (!PathFileExistsA(path.c_str())) CreateDirectoryA(path.c_str(), NULL); data_path = path; } else { CHAR path[MAX_PATH]; GetCurrentDirectoryA(sizeof(path), path); data_path = path; } rec_path = data_path + "\\frames"; if (!PathFileExistsA(rec_path.c_str())) CreateDirectoryA(rec_path.c_str(), NULL); if (!PathFileExistsA((data_path + "\\brushes").c_str())) CreateDirectoryA((data_path + "\\brushes").c_str(), NULL); if (!PathFileExistsA((data_path + "\\brushes\\thumbs").c_str())) CreateDirectoryA((data_path + "\\brushes\\thumbs").c_str(), NULL); if (!PathFileExistsA((data_path + "\\patterns").c_str())) CreateDirectoryA((data_path + "\\patterns").c_str(), NULL); if (!PathFileExistsA((data_path + "\\patterns\\thumbs").c_str())) CreateDirectoryA((data_path + "\\patterns\\thumbs").c_str(), NULL); if (!PathFileExistsA((data_path + "\\settings").c_str())) CreateDirectoryA((data_path + "\\settings").c_str(), NULL); #endif // TODO: save this path somewhere in the settings, don't overwrite every start work_path = data_path; //LogRemote::I.start(); LogRemote::I.file_init(); LOG("%s", g_version); LOG("load preferences"); if (!Settings::load()) LOG("load preferences failed"); } 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; } void App::download(std::string url, std::string dest_filepath, std::function progress) { 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); } } bool App::check_license() { return true; // TODO: for distribuiton only 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; } return false; } void App::upload(std::string filename, std::string name, std::function progress) { 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); } } #ifdef _WIN32 static CONSOLE_SCREEN_BUFFER_INFO info; void handle_gl_callback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* message, const void* userParam) { static std::map colors = { { GL_DEBUG_SEVERITY_NOTIFICATION, 8 }, { GL_DEBUG_SEVERITY_LOW, 8 }, { GL_DEBUG_SEVERITY_MEDIUM, FOREGROUND_GREEN | FOREGROUND_INTENSITY }, { GL_DEBUG_SEVERITY_HIGH, FOREGROUND_RED | FOREGROUND_INTENSITY }, }; if (severity == GL_DEBUG_SEVERITY_HIGH || severity == GL_DEBUG_SEVERITY_MEDIUM || severity == GL_DEBUG_SEVERITY_LOW) { SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), colors[severity]); LOG("OPENGL: %.*s", length, message); FlushConsoleInputBuffer(GetStdHandle(STD_OUTPUT_HANDLE)); SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), info.wAttributes); #ifdef _DEBUG if (severity == GL_DEBUG_SEVERITY_HIGH) __debugbreak(); #endif } } #endif void App::init() { #ifdef _WIN32 if (glDebugMessageCallback) { // colors: http://stackoverflow.com/questions/4053837/colorizing-text-in-the-console-with-c GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info); render_task([] { glDebugMessageCallback(handle_gl_callback, nullptr); glEnable(GL_DEBUG_OUTPUT); glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); }); } #endif LOG("Screen Resolution: %dx%d", (int)width, (int)height); render_task([] { LOG("GL version: %s", glGetString(GL_VERSION)); LOG("GL vendor: %s", glGetString(GL_VENDOR)); LOG("GL renderer: %s", glGetString(GL_RENDERER)); //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)); // } //} glDisable(GL_DEPTH_TEST); #if defined(_WIN32) || defined(__OSX__) glEnable(GL_PROGRAM_POINT_SIZE); glEnable(GL_LINE_SMOOTH); #endif glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBlendEquationSeparate(GL_FUNC_ADD, GL_MAX); }); 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(); if (!check_license()) { message_box("License", "Could not validate this license, running in demo mode."); } } void App::async_start() { #if __OSX__ [osx_view async_lock]; #elif __IOS__ [ios_view async_lock]; #elif __ANDROID__ android_async_lock(and_engine); #elif _WIN32 async_lock(); glBindFramebuffer(GL_FRAMEBUFFER, 0); #endif } void App::async_update() { #if __IOS__ [ios_view->glview bindDrawable]; #elif _WIN32 glBindFramebuffer(GL_FRAMEBUFFER, 0); #endif redraw = true; clear(); update(0); #if __OSX__ [osx_view async_swap]; #elif __IOS__ [ios_view->glview bindDrawable]; [ios_view async_swap]; #elif __ANDROID__ android_async_swap(and_engine); #elif _WIN32 async_swap(); #endif } void App::async_redraw() { redraw = true; } void App::async_end() { #if __OSX__ [osx_view async_unlock]; #elif __IOS__ [ios_view async_unlock]; #elif __ANDROID__ android_async_unlock(and_engine); #elif _WIN32 async_unlock(); #endif } void App::async_swap() { #if __OSX__ [osx_view async_swap]; #elif __IOS__ [ios_view async_swap]; #elif __ANDROID__ android_async_swap(and_engine); #elif _WIN32 win32_async_swap(); #endif } 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::vec4 pclip = { xy(p->m_clip_uncut) + off_p, zw(p->m_clip_uncut) - off_s - off_p }; 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; if (box.z <= 0.f || box.w <= 0.f) { 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((int)box.x, (int)(height / zoom - box.y - box.w), (int)box.z, (int)box.w) * zoom; glScissor(c.x + off_x, c.y + off_y, c.z, 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(); glViewport(0, 0, uirtt.getWidth(), uirtt.getHeight()); glEnable(GL_SCISSOR_TEST); for (int i = 1; i < layout[main_id]->m_children.size(); i++) layout[main_id]->m_children[i]->watch(observer); //msgbox->watch(observer); glDisable(GL_SCISSOR_TEST); uirtt.unbindFramebuffer(); } if (!vr_only) { #if __IOS__ [ios_view->glview bindDrawable]; #else glBindFramebuffer(GL_FRAMEBUFFER, 0); #endif glViewport(off_x, off_y, (GLsizei)width, (GLsizei)height); glEnable(GL_SCISSOR_TEST); for (int i = 0; i < layout[main_id]->m_children.size(); i++) layout[main_id]->m_children[i]->watch(observer); //msgbox->watch(observer); glDisable(GL_SCISSOR_TEST); } if (rec_running) { static float rec_timer = 0.f; rec_timer += dt; if (rec_timer > 1.f && canvas->m_canvas->m_dirty_stroke) { canvas->m_canvas->m_dirty_stroke = false; LOG("rec tick"); rec_timer = 0.f; auto data = new uint8_t[width * height * 4]; #if __IOS__ [ios_view->glview bindDrawable]; #else glBindFramebuffer(GL_FRAMEBUFFER, 0); #endif GLint dfbo, rfbo; glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &rfbo); glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &dfbo); if (dfbo != rfbo) LOG("DIFFERENT FB"); glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, data); { std::lock_guard lock(rec_mutex); rec_frames.emplace_back(data); rec_cv.notify_all(); } update_rec_frames(); } } redraw = false; } void App::update(float dt) { static std::mutex mutex; static float reload_timer = 0.f; // avoid multiple threads to update the scene //std::lock_guard lock(mutex); if (!(redraw || animate)) return; #if /*_DEBUG &&*/ (_WIN32 || __OSX__) reload_timer += dt; if (reload_timer > 1.0) { reload_timer = 0; if (ShaderManager::reload()) stroke->update_controls(); layout.reload(); } #endif if (auto* main = layout[main_id]) main->update(width, height, zoom); { static glm::vec4 color_button_normal{ .1, .1, .1, 1 }; static glm::vec4 color_button_hlight{ 1, .0, .0, 1 }; auto mode = Canvas::I->m_current_mode; CanvasModePen* pm = (CanvasModePen*)canvas->m_canvas->modes[(int)kCanvasMode::Draw][0]; layout[main_id]->find("btn-pick")->set_active(mode == kCanvasMode::Draw && pm->m_picking); layout[main_id]->find("btn-touchlock")->set_active(canvas->m_canvas->m_touch_lock); layout[main_id]->find("btn-pen")->set_active(mode == kCanvasMode::Draw); layout[main_id]->find("btn-erase")->set_active(mode == kCanvasMode::Erase); layout[main_id]->find("btn-cam")->set_active(mode == kCanvasMode::Camera); layout[main_id]->find("btn-line")->set_active(mode == kCanvasMode::Line); layout[main_id]->find("btn-grid")->set_active(mode == kCanvasMode::Grid); layout[main_id]->find("btn-copy")->set_active(mode == kCanvasMode::Copy); layout[main_id]->find("btn-cut")->set_active(mode == kCanvasMode::Cut); layout[main_id]->find("btn-mask-free")->set_active(mode == kCanvasMode::MaskFree); layout[main_id]->find("btn-mask-line")->set_active(mode == kCanvasMode::MaskLine); layout[main_id]->find("btn-bucket")->set_active(mode == kCanvasMode::FloodFill); } } void App::terminate() { LOG("App::terminate"); ui_save(); NodeStrokePreview::terminate_renderer(); rec_stop(); TextureManager::invalidate(); ShaderManager::invalidate(); layout.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")) { static char buffer[128]; sprintf(buffer, "History memory: %.2f Mb", bytes / 1024.f / 1024.f); txt->set_text(buffer); } } void App::update_rec_frames() { if (auto txt = layout[main_id]->find("txt-rec")) { if (rec_running) { static char buffer[128]; sprintf(buffer, "Recorder %d frames", rec_count); txt->set_text(buffer); } else { txt->set_text(""); } layout[main_id]->update(); } } int App::res_from_index(int i) { return res_map[i]; } int App::res_to_index(int res) { return (int)std::distance(res_map.begin(), std::find(res_map.begin(), res_map.end(), res)); } std::string App::res_to_string(int res) { return res_map_str[res_to_index(res)]; } void App::rec_clear() { rec_stop(); #if defined(__IOS__) || defined(__OSX__) NSString *path = [NSString stringWithUTF8String:rec_path.c_str()]; NSDirectoryEnumerator* en = [[NSFileManager defaultManager] enumeratorAtPath:path]; NSError* err = nil; BOOL res; NSString* file; while (file = [en nextObject]) { NSString* file_path = [path stringByAppendingPathComponent:file]; [[NSFileManager defaultManager] removeItemAtPath:file_path error:nil]; NSLog(@"delete: %@", file_path); } #endif rec_count = 0; update_rec_frames(); } void App::rec_start() { if (!rec_running) { #if defined(__IOS__) || defined(__OSX__) NSString* path = [NSString stringWithUTF8String:rec_path.c_str()]; NSArray *dirFiles = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:nil]; NSArray *jpgFiles = [dirFiles filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"self ENDSWITH '.jpg'"]]; rec_count = (int)[jpgFiles count]; update_rec_frames(); rec_thread = std::thread(&App::rec_loop, this); #else rec_count = Asset::list_files(rec_path, ".*\\.jpg").size(); update_rec_frames(); rec_thread = std::thread(&App::rec_loop, this); #endif } } void App::rec_stop() { if (rec_running) { rec_running = false; rec_cv.notify_all(); if (rec_thread.joinable()) rec_thread.join(); update_rec_frames(); } } void stillImageDataReleaseCallback(void *releaseRefCon, const void *baseAddress) { free((void *)baseAddress); } void App::rec_export(std::string path) { int progress = 0; int tot = rec_count; auto pb = layout[main_id]->add_child(); pb->m_progress->SetWidthP(0); pb->m_title->set_text("Exporting MP4 movie"); #if defined(__IOS__) || defined(__OSX__) NSString* mov_path = [NSString stringWithFormat:@"%s/out.mp4", rec_path.c_str()]; if ([[NSFileManager defaultManager] fileExistsAtPath:mov_path]) { NSLog(@"remove existing mp4"); [[NSFileManager defaultManager] removeItemAtPath:mov_path error:nil]; } NSURL* url = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%s/out.mp4", rec_path.c_str()]]; AVAssetWriter* writer = [AVAssetWriter assetWriterWithURL:url fileType:AVFileTypeMPEG4 error:nil]; writer.shouldOptimizeForNetworkUse = NO; NSDictionary *videoCompressionSettings = @{ AVVideoCodecKey : AVVideoCodecH264, AVVideoWidthKey : @(width), AVVideoHeightKey : @(height), AVVideoCompressionPropertiesKey : @{ AVVideoAverageBitRateKey : @(8<<20) } }; if (![writer canApplyOutputSettings:videoCompressionSettings forMediaType:AVMediaTypeVideo]) { NSLog(@"Couldn't add asset writer video input."); return; } AVAssetWriterInput* input = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoCompressionSettings sourceFormatHint:nil]; input.expectsMediaDataInRealTime = YES; NSDictionary *adaptorDict = @{ (id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA), (id)kCVPixelBufferWidthKey : @(width), (id)kCVPixelBufferHeightKey : @(height) }; AVAssetWriterInputPixelBufferAdaptor* _pixelBufferAdaptor = [[AVAssetWriterInputPixelBufferAdaptor alloc] initWithAssetWriterInput:input sourcePixelBufferAttributes:adaptorDict]; // Add asset writer input to asset writer if (![writer canAddInput:input]) { NSLog(@"Couldn't add input to writer."); return; } [writer addInput:input]; CMTime t; t.timescale = 30; t.flags = kCMTimeFlags_Valid; t.epoch = 0; t.value = 0; //[writer startSessionAtSourceTime:t]; CVPixelBufferRef buff = NULL; uint8_t* data = (uint8_t*)calloc(1, width * height * 4); CVPixelBufferCreateWithBytes(kCFAllocatorDefault, width, height, kCVPixelFormatType_32RGBA, data, width * 4, stillImageDataReleaseCallback, nil, nil, &buff); OSStatus err = CVPixelBufferPoolCreatePixelBuffer(nil, _pixelBufferAdaptor.pixelBufferPool, &buff); if (writer.status == AVAssetWriterStatusUnknown) { // If the asset writer status is unknown, implies writing hasn't started yet, hence start writing with start time as the buffer's presentation timestamp if ([writer startWriting]) { [writer startSessionAtSourceTime:t]; } } if (writer.status == AVAssetWriterStatusWriting) { for (int i = 0; i < tot; i++) { // If the asset writer status is writing, append sample buffer to its corresponding asset writer input if (input.readyForMoreMediaData) { char path[256]; snprintf(path, sizeof(path), "%s/%04d.jpg", rec_path.c_str(), i); NSString* img_path = [NSString stringWithUTF8String:path]; NSLog(@"frame: %@", img_path); #if __OSX__ NSImage *image = [[NSImage alloc] initWithContentsOfFile:img_path]; if (!image) break; NSRect imageRect = NSMakeRect(0, 0, image.size.width, image.size.height); CGImageRef cgImage = [image CGImageForProposedRect:&imageRect context:NULL hints:nil]; #elif __IOS__ UIImage* image = [UIImage imageNamed:img_path]; if (!image) break; CGImageRef cgImage = image.CGImage; #endif NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool:YES], kCVPixelBufferCGImageCompatibilityKey, [NSNumber numberWithBool:YES], kCVPixelBufferCGBitmapContextCompatibilityKey, nil]; CGSize sz = image.size; CVPixelBufferRef pxbuffer = NULL; { CGImageRef image = cgImage; CGSize size = sz; CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault, size.width, size.height, kCVPixelFormatType_32ARGB, (__bridge CFDictionaryRef) options, &pxbuffer); // CVReturn status = CVPixelBufferPoolCreatePixelBuffer(NULL, adaptor.pixelBufferPool, &pxbuffer); //NSParameterAssert(status == kCVReturnSuccess && pxbuffer != NULL); CVPixelBufferLockBaseAddress(pxbuffer, 0); void *pxdata = CVPixelBufferGetBaseAddress(pxbuffer); //NSParameterAssert(pxdata != NULL); CGColorSpaceRef rgbColorSpace = CGColorSpaceCreateDeviceRGB(); CGContextRef context = CGBitmapContextCreate(pxdata, size.width, size.height, 8, 4*size.width, rgbColorSpace, kCGImageAlphaPremultipliedFirst); //NSParameterAssert(context); CGContextDrawImage(context, CGRectMake(0, 0, CGImageGetWidth(image), CGImageGetHeight(image)), image); CGColorSpaceRelease(rgbColorSpace); CGContextRelease(context); CVPixelBufferUnlockBaseAddress(pxbuffer, 0); } t.value = i; if (![_pixelBufferAdaptor appendPixelBuffer:pxbuffer withPresentationTime:t]) { NSLog(@"error %@", [writer.error localizedFailureReason]); } CFRelease(pxbuffer); progress++; pb->m_progress->SetWidthP((float)progress / tot * 100.f); async_update(); } } [input markAsFinished]; [writer finishWritingWithCompletionHandler:^{ NSString* path = [NSString stringWithFormat:@"%s/out.mp4", rec_path.c_str()]; NSLog(@"saved video %@", path); #if __IOS__ if (UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(path)) { NSLog(@"saving to camera roll"); UISaveVideoAtPathToSavedPhotosAlbum(path, nil, nil, nil); } #endif }]; } if (writer.status == AVAssetWriterStatusFailed) { NSLog(@"failed"); } #endif pb->destroy(); } void App::rec_loop() { BT_SetTerminate(); rec_running = true; while(rec_running) { std::unique_lock lock(rec_mutex); rec_cv.wait(lock); if (!rec_running) break; if (!rec_frames.empty()) { if (rec_frames.front()) { auto inverted = std::make_unique(width*height*4); for (int y = height - 1, y1 = 0; y >= 0; y--, y1++) { uint8_t* dst = &inverted[y * width * 4]; uint8_t* src = &rec_frames.front()[y1 * width * 4]; std::copy_n(src, (int)width * 4, dst); } char path[256]; snprintf(path, sizeof(path), "%s/%04d.jpg", rec_path.c_str(), rec_count); LOG("writing %s", path); jpge::params params; params.m_quality = 75; bool saved = jpge::compress_image_to_jpeg_file(path, width, height, 4, inverted.get(), params); if (!saved) { LOG("error writing the frame"); rec_running = false; } else { rec_count++; redraw = true; } } rec_frames.pop_front(); } } } 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); count++; working_list.front()(); working_list.pop_front(); } async_end(); } } } void App::ui_thread_main() { BT_SetTerminate(); uint32_t count = 0; ui_thread_id = std::this_thread::get_id(); ui_running = true; init(); auto t_start = std::chrono::high_resolution_clock::now(); float t_frame = 0; float t_fps_counter = 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(100), [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; #ifdef _WIN32 extern void win32_update_stylus(float dt); win32_update_stylus(dt); #endif // increment timers t_frame += dt; t_fps_counter += dt; if (t_fps_counter > 1.f) { #ifdef _WIN32 extern void win32_update_fps(int frames); win32_update_fps(rendered_frames); #endif t_fps_counter = 0; rendered_frames = 0; } tick(dt); if (redraw) { update(t_frame); render_task([this, t_frame] { glBindFramebuffer(GL_FRAMEBUFFER, 0); clear(); draw(t_frame); async_swap(); }); t_frame = 0; rendered_frames++; } } } void App::render_thread_start() { render_thread = std::thread(&App::render_thread_main, this); } 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); } void App::ui_thread_stop() { ui_running = false; ui_cv.notify_all(); if (ui_thread.joinable()) ui_thread.join(); }