#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 #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 void async_lock(); void async_swap(); void async_unlock(); void destroy_window(); #endif using namespace ui; App App::I; // singleton void App::create() { width = 1920/2; height = 1080/2; } bool App::request_close() { static bool dialog_already_opened = false; if (!ui::Canvas::I->m_unsaved) return true; if (!dialog_already_opened) { async_start(); auto* m = layout[main_id]->add_child(); m->m_title->set_text("Unsaved document"); m->m_message->set_text("Would you like to save before closing?"); m->btn_ok->m_text->set_text("Yes"); m->btn_ok->on_click = [](Node*) { }; m->btn_cancel->m_text->set_text("No"); m->btn_cancel->on_click = [this](Node*) { #ifdef _WIN32 destroy_window(); PostQuitMessage(0); #endif #ifdef __OSX__ [osx_view close]; #endif ui::Canvas::I->m_unsaved = false; }; async_redraw(); async_end(); dialog_already_opened = true; } return false; } void App::clear() { glClearColor(.1f, .1f, .1f, 1.f); glViewport(0, 0, (GLsizei)width, (GLsizei)height); glClear(GL_COLOR_BUFFER_BIT); } void App::initAssets() { LOG("initializing assets"); FontManager::init(); LOG("initializing assets loading fonts"); FontManager::load(kFont::Arial_11, "data/arial.ttf", 15); FontManager::load(kFont::Arial_30, "data/arial.ttf", 30); LOG("initializing assets create sampler"); sampler.create(GL_NEAREST); LOG("initializing assets load uvs texture"); if (!tex.load("data/uvs.jpg")) LOG("error loading image"); LOG("initializing assets completed"); } void App::initLog() { #if defined(__IOS__) || defined(__OSX__) NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString* docpath = [paths objectAtIndex:0]; data_path = [docpath cStringUsingEncoding:NSASCIIStringEncoding]; NSString* recpath = [docpath stringByAppendingString:@"/rec"]; rec_path = [recpath cStringUsingEncoding:NSASCIIStringEncoding]; NSError* recerr = nil; if (![[NSFileManager defaultManager] createDirectoryAtPath:recpath withIntermediateDirectories:YES attributes:nil error:&recerr]) { LOG("error creating rec path: %s", [[recerr localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]); } #elif _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 path[MAX_PATH]; GetCurrentDirectoryA(sizeof(path), path); data_path = path; rec_path = data_path + "\\frames"; #endif LogRemote::I.start(); LogRemote::I.file_init(); } 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 filename, std::function progress) { CURL *curl = curl_easy_init(); if (curl) { auto dest = data_path + "/" + filename; FILE* fp = fopen(dest.c_str(), "wb"); std::string url = "http://omigamedev.com/panopainter/cloud/cloud-dwl.php?file=" + filename; 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); 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() { CURL *curl = curl_easy_init(); if (curl) { std::string url = "http://omigamedev.com/panopainter/79516B99-8E67-40AD-B12F-149A5A9C2E15"; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L); auto err = curl_easy_perform(curl); curl_easy_cleanup(curl); if (err == CURLcode::CURLE_OK) 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 = "http://omigamedev.com/panopainter/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); 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); } } void App::init() { #ifdef _WIN32 static CONSOLE_SCREEN_BUFFER_INFO info; GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info); // colors: http://stackoverflow.com/questions/4053837/colorizing-text-in-the-console-with-c glDebugMessageCallback([](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); //__debugbreak(); } }, nullptr); glEnable(GL_DEBUG_OUTPUT); glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS); #endif 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++) // { // LOG("%s", glGetStringi(GL_EXTENSIONS, i)); // } LOG("Screen Resolution: %dx%d", (int)width, (int)height); //zoom = ceilf(width / 2000.f); //zoom = 2; glDisable(GL_DEPTH_TEST); #if defined(_WIN32) || defined(__OSX__) glEnable(GL_PROGRAM_POINT_SIZE); #endif glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBlendEquation(GL_FUNC_ADD); initShaders(); initAssets(); initLayout(); GLfloat width_range[2]; glGetFloatv(GL_ALIASED_LINE_WIDTH_RANGE, width_range); LOG("GL line range: %f - %f", width_range[0], width_range[1]); LOG("Screen Size: %f %f", width, height); GLint fb0; glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &fb0); LOG("Default Framebuffer %d", fb0); } 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::update(float dt) { static float rec_timer = 0.f; static std::mutex m; std::lock_guard _lock(m); // update offscreen stuff if (canvas && canvas->m_canvas) canvas->m_canvas->stroke_draw(); if (!(redraw || animate)) return; //glClearColor(.1f, .1f, .1f, 1.f); //glViewport(0, 0, (GLsizei)width, (GLsizei)height); //glClear(GL_COLOR_BUFFER_BIT); //if (!canvas->m_mouse_captured) { #if _WIN32 || __OSX__ layout.reload(); #endif if (auto* main = layout[main_id]) { main->update(width, height, zoom); stroke->update_controls(); } } static glm::vec4 color_button_normal{.1, .1, .1, 1}; static glm::vec4 color_button_hlight{ 1, .0, .0, 1}; CanvasModePen* mode = (CanvasModePen*)canvas->m_canvas->modes[(int)Canvas::kCanvasMode::Draw][0]; layout[main_id]->find("btn-pick")->set_color( mode->m_picking ? color_button_hlight : color_button_normal); layout[main_id]->find("btn-pen")->set_color( canvas->m_canvas->m_current_mode == Canvas::kCanvasMode::Draw ? color_button_hlight : color_button_normal); layout[main_id]->find("btn-erase")->set_color( canvas->m_canvas->m_current_mode == Canvas::kCanvasMode::Erase ? color_button_hlight : color_button_normal); layout[main_id]->find("btn-touchlock")->set_color( canvas->m_canvas->m_touch_lock ? color_button_hlight : color_button_normal); auto observer = [this](Node* n) { if (n && n->m_display) { 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::ivec4 c = glm::vec4((int)box.x, (int)(height / zoom - box.y - box.w), (int)box.z, (int)box.w) * zoom; glScissor(c.x, c.y, c.z, c.w); n->draw(); return true; } return false; }; //glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); #if __IOS__ [ios_view->glview bindDrawable]; #else glBindFramebuffer(GL_FRAMEBUFFER, 0); #endif glEnable(GL_SCISSOR_TEST); if (auto* main = layout[main_id]) main->watch(observer); //msgbox->watch(observer); glDisable(GL_SCISSOR_TEST); if (rec_running) { 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"); glReadBuffer(GL_FRONT); 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::terminate() { LOG("App::terminate"); TextureManager::invalidate(); ShaderManager::invalidate(); layout.clear_context(); brushes->clear_context(); layers->clear_context(); color->clear_context(); stroke->clear_context(); rec_stop(); } 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); layout[main_id]->update(); } } 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(); } } 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, false, ".*\\.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"); async_update(); #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(); async_update(); } void App::rec_loop() { 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(); } } }