diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 856a836..38ab730 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -19,7 +19,7 @@ android:name="io.github.nexoniarz.fry3dx.FryActivity" android:exported="true" android:label="Fry3DX" - android:screenOrientation="fullUser" + android:screenOrientation="sensorLandscape" android:launchMode="singleInstance" android:windowSoftInputMode="adjustResize" android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"> diff --git a/engine/engine.cpp b/engine/engine.cpp index 1faad39..4b6b6d5 100644 --- a/engine/engine.cpp +++ b/engine/engine.cpp @@ -371,6 +371,11 @@ int runClient(Game& game) { if (!input::captured()) ImGui_ImplSDL3_ProcessEvent(&ev); if (ev.type == SDL_EVENT_QUIT && game.allowQuit()) g_quit = true; if (ev.type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED) renderer.onResize(); + // (a phone: SDL waits once these are seen, until the app's back; the surface is new then) + if (ev.type == SDL_EVENT_WILL_ENTER_BACKGROUND || ev.type == SDL_EVENT_LOW_MEMORY || ev.type == SDL_EVENT_TERMINATING) game.onBackground(); +#if defined(__ANDROID__) + if (ev.type == SDL_EVENT_DID_ENTER_FOREGROUND) renderer.onSurfaceLost(); +#endif } const bool focused = SDL_GetWindowFlags(window.handle()) & SDL_WINDOW_INPUT_FOCUS; audio::setFocused(focused); diff --git a/engine/engine.h b/engine/engine.h index b0f6ab3..15f70cc 100644 --- a/engine/engine.h +++ b/engine/engine.h @@ -36,6 +36,9 @@ public: // A file dropped on the window, or opened with the program from the desktop (a Mac's Finder // hands it over this way, not on the command line). virtual void onFileDropped(const std::string& path) { (void)path; } + // Going into the background (a phone: another app, the home screen), or the system is short + // of memory: the work should be kept now, as the app may be closed while it's away. + virtual void onBackground() {} // Hot reload: called twice a second (hotreload 1) after the engine reloaded what changed // (`changed`: textures, models, sounds); the game reloads its own (scripts, the map). virtual void hotReload(const std::vector& changed) { (void)changed; } diff --git a/engine/render/backend.h b/engine/render/backend.h index 3cf9b49..bac50ab 100644 --- a/engine/render/backend.h +++ b/engine/render/backend.h @@ -218,6 +218,8 @@ public: virtual void beginUi() = 0; virtual void setVsync(bool on) = 0; virtual void onResize() = 0; + // Back from the background (a phone): the window's surface may be a new one. + virtual void onSurfaceLost() { onResize(); } // A texture in table slot `slot`, mips largest first: RGBA8 pixels, or BC7 blocks (YAPF 2's // GPU payload) when blockCompression() says the GPU takes them. virtual void createTexture(TextureId slot, uint32_t width, uint32_t height, bool srgb, const std::vector>& mips, diff --git a/engine/render/renderer.h b/engine/render/renderer.h index 89bc5bd..fb60aac 100644 --- a/engine/render/renderer.h +++ b/engine/render/renderer.h @@ -157,6 +157,9 @@ public: void onResize() { if (backend_) backend_->onResize(); } + void onSurfaceLost() { + if (backend_) backend_->onSurfaceLost(); + } private: struct Submesh { diff --git a/engine/rhi/vulkan/vk_backend.cpp b/engine/rhi/vulkan/vk_backend.cpp index e599075..7042540 100644 --- a/engine/rhi/vulkan/vk_backend.cpp +++ b/engine/rhi/vulkan/vk_backend.cpp @@ -404,6 +404,7 @@ public: } void setVsync(bool on) override { device_.setVsync(on); } void onResize() override { device_.markResized(); } + void onSurfaceLost() override { device_.markSurfaceLost(); } bool renderScale() const override { return true; } bool blockCompression() const override { diff --git a/engine/rhi/vulkan/vk_device.cpp b/engine/rhi/vulkan/vk_device.cpp index eed0e5f..7cd3c1b 100644 --- a/engine/rhi/vulkan/vk_device.cpp +++ b/engine/rhi/vulkan/vk_device.cpp @@ -263,6 +263,24 @@ bool Device::createSwapchain() { return true; } +// A new surface on the window as it is now (Android makes a new one each time the app comes back). +bool Device::recreateSurface() { + vkDeviceWaitIdle(device_); + destroySwapchainViews(); + if (swapchain_) vkDestroySwapchainKHR(device_, swapchain_, nullptr), swapchain_ = VK_NULL_HANDLE; + if (surface_) vkDestroySurfaceKHR(instance_, surface_, nullptr), surface_ = VK_NULL_HANDLE; + if (!SDL_Vulkan_CreateSurface(window_->handle(), instance_, nullptr, &surface_)) { + log::warn("SDL_Vulkan_CreateSurface (again): {}", SDL_GetError()); + surface_ = VK_NULL_HANDLE; + return false; // (tried again next frame) + } + VkBool32 present = VK_FALSE; + vkGetPhysicalDeviceSurfaceSupportKHR(physical_, queueFamily_, surface_, &present); + if (!present) log::warn("The new surface can't be presented to from this queue"); + surfaceLost_ = false; + return createSwapchain(); +} + void Device::destroySwapchainViews() { for (VkImageView v : views_) vkDestroyImageView(device_, v, nullptr); for (VkSemaphore s : renderDone_) vkDestroySemaphore(device_, s, nullptr); @@ -271,6 +289,7 @@ void Device::destroySwapchainViews() { } VkCommandBuffer Device::beginFrame() { + if (surfaceLost_ && !recreateSurface()) return nullptr; if (swapchainDirty_ && !createSwapchain()) return nullptr; Frame& f = frames_[frameIndex_]; @@ -280,6 +299,10 @@ VkCommandBuffer Device::beginFrame() { swapchainDirty_ = true; return nullptr; } + if (r == VK_ERROR_SURFACE_LOST_KHR) { + surfaceLost_ = true; + return nullptr; + } if (r != VK_SUBOPTIMAL_KHR) VK_CHECK(r); VK_CHECK(vkResetFences(device_, 1, &f.inFlight)); @@ -328,6 +351,7 @@ void Device::endFrame() { } #endif if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR) swapchainDirty_ = true; + else if (r == VK_ERROR_SURFACE_LOST_KHR) surfaceLost_ = true; else VK_CHECK(r); frameIndex_ = (frameIndex_ + 1) % kFramesInFlight; diff --git a/engine/rhi/vulkan/vk_device.h b/engine/rhi/vulkan/vk_device.h index 61c7eea..e138cc3 100644 --- a/engine/rhi/vulkan/vk_device.h +++ b/engine/rhi/vulkan/vk_device.h @@ -50,6 +50,8 @@ public: void setVsync(bool vsync) { vsync_ = vsync; swapchainDirty_ = true; } void markResized() { swapchainDirty_ = true; } + // The window's surface went away (a phone: the app was in the background): made again. + void markSurfaceLost() { surfaceLost_ = true; } void waitIdle() { vkDeviceWaitIdle(device_); } VkInstance instance() const { return instance_; } @@ -81,6 +83,7 @@ private: }; bool createSwapchain(); + bool recreateSurface(); void destroySwapchainViews(); const Window* window_ = nullptr; @@ -102,7 +105,7 @@ private: std::vector views_; std::vector renderDone_; // per swapchain image, so present never reuses one in flight bool vsync_ = true; - bool swapchainDirty_ = false; + bool swapchainDirty_ = false, surfaceLost_ = false; Frame frames_[kFramesInFlight]; VkCommandPool immPool_ = VK_NULL_HANDLE; diff --git a/src/app.cpp b/src/app.cpp index e972808..f9bbeff 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -610,10 +610,51 @@ public: loadPlugins(); if (!startPath_.empty()) openFile(startPath_); findAutosaves(); + if (startPath_.empty()) resumeWork(); + } + + // Going into the background: the work is saved now (a phone may close the app while it's + // away, to free memory), and a phone's next start carries on with it, view and all. + std::string resumeFile() const { return keel::prefPath() + "resume.txt"; } + void onBackground() override { + if (dirty_) autosave(true); + if (autosaveJob_.valid()) autosaveJob_.wait(); +#if defined(__ANDROID__) + std::ofstream(resumeFile()) << (dirty_ ? autosavePath_ : std::string()) << '\n' + << path_ << '\n' + << int(tab_) << ' ' << target_.x << ' ' << target_.y << ' ' << target_.z << ' ' << yaw_ << ' ' << pitch_ << ' ' << dist_ << '\n'; +#endif + } + void resumeWork() { +#if defined(__ANDROID__) + std::ifstream in(resumeFile()); + if (!in) return; + std::string saved, from; + std::getline(in, saved), std::getline(in, from); + int tab = 0; + glm::vec3 target = target_; + float yaw = yaw_, pitch = pitch_, dist = dist_; + const bool view = bool(in >> tab >> target.x >> target.y >> target.z >> yaw >> pitch >> dist); + in.close(); + std::error_code ec; + stdfs::remove(resumeFile(), ec); + if (!saved.empty() && stdfs::exists(saved, ec)) { // (unsaved work: as recovering it, without asking) + openFile(saved); + path_ = from, dirty_ = true, autosavePath_ = saved; + showRecover_ = false; + } else if (!from.empty()) + openFile(from); + else + return; + if (view) setTab(Tab(std::clamp(tab, 0, 4))), target_ = target, yaw_ = yaw, pitch_ = std::clamp(pitch, -89.0f, 89.0f), dist_ = std::max(dist, 0.01f); + say("Carried on where you left off"); +#endif } void frame(double) override { autosave(); } bool allowQuit() override { + std::error_code ec; + stdfs::remove(resumeFile(), ec); // (closed on purpose: the next start is a fresh one) if (!dirty_) return dropAutosave(), true; askSave_ = 1; return false; @@ -7282,6 +7323,9 @@ int main(int argc, char** argv) { if (std::string(argv[i]).ends_with(".fry") && argv[i - 1][0] != '+') path = argv[i]; // (not a console command's own) else rest.push_back(argv[i]); } +#if defined(__ANDROID__) // (a phone: sideways only; SDL would otherwise let a resizable window turn upright) + SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight"); +#endif App app(path); return keel::run(app, int(rest.size()), rest.data()); }