Phones: sideways only; back from the background the Vulkan surface is made again (it was lost), the work saved on the way out and carried on with on the next start if the phone closed it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-10-21 00:16:22 +02:00
commit a79a6c9020
9 changed files with 87 additions and 2 deletions

View file

@ -19,7 +19,7 @@
android:name="io.github.nexoniarz.fry3dx.FryActivity" android:name="io.github.nexoniarz.fry3dx.FryActivity"
android:exported="true" android:exported="true"
android:label="Fry3DX" android:label="Fry3DX"
android:screenOrientation="fullUser" android:screenOrientation="sensorLandscape"
android:launchMode="singleInstance" android:launchMode="singleInstance"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"> android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation">

View file

@ -371,6 +371,11 @@ int runClient(Game& game) {
if (!input::captured()) ImGui_ImplSDL3_ProcessEvent(&ev); if (!input::captured()) ImGui_ImplSDL3_ProcessEvent(&ev);
if (ev.type == SDL_EVENT_QUIT && game.allowQuit()) g_quit = true; if (ev.type == SDL_EVENT_QUIT && game.allowQuit()) g_quit = true;
if (ev.type == SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED) renderer.onResize(); 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; const bool focused = SDL_GetWindowFlags(window.handle()) & SDL_WINDOW_INPUT_FOCUS;
audio::setFocused(focused); audio::setFocused(focused);

View file

@ -36,6 +36,9 @@ public:
// A file dropped on the window, or opened with the program from the desktop (a Mac's Finder // 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). // hands it over this way, not on the command line).
virtual void onFileDropped(const std::string& path) { (void)path; } 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 // 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). // (`changed`: textures, models, sounds); the game reloads its own (scripts, the map).
virtual void hotReload(const std::vector<std::string>& changed) { (void)changed; } virtual void hotReload(const std::vector<std::string>& changed) { (void)changed; }

View file

@ -218,6 +218,8 @@ public:
virtual void beginUi() = 0; virtual void beginUi() = 0;
virtual void setVsync(bool on) = 0; virtual void setVsync(bool on) = 0;
virtual void onResize() = 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 // 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. // 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<std::vector<uint8_t>>& mips, virtual void createTexture(TextureId slot, uint32_t width, uint32_t height, bool srgb, const std::vector<std::vector<uint8_t>>& mips,

View file

@ -157,6 +157,9 @@ public:
void onResize() { void onResize() {
if (backend_) backend_->onResize(); if (backend_) backend_->onResize();
} }
void onSurfaceLost() {
if (backend_) backend_->onSurfaceLost();
}
private: private:
struct Submesh { struct Submesh {

View file

@ -404,6 +404,7 @@ public:
} }
void setVsync(bool on) override { device_.setVsync(on); } void setVsync(bool on) override { device_.setVsync(on); }
void onResize() override { device_.markResized(); } void onResize() override { device_.markResized(); }
void onSurfaceLost() override { device_.markSurfaceLost(); }
bool renderScale() const override { return true; } bool renderScale() const override { return true; }
bool blockCompression() const override { bool blockCompression() const override {

View file

@ -263,6 +263,24 @@ bool Device::createSwapchain() {
return true; 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() { void Device::destroySwapchainViews() {
for (VkImageView v : views_) vkDestroyImageView(device_, v, nullptr); for (VkImageView v : views_) vkDestroyImageView(device_, v, nullptr);
for (VkSemaphore s : renderDone_) vkDestroySemaphore(device_, s, nullptr); for (VkSemaphore s : renderDone_) vkDestroySemaphore(device_, s, nullptr);
@ -271,6 +289,7 @@ void Device::destroySwapchainViews() {
} }
VkCommandBuffer Device::beginFrame() { VkCommandBuffer Device::beginFrame() {
if (surfaceLost_ && !recreateSurface()) return nullptr;
if (swapchainDirty_ && !createSwapchain()) return nullptr; if (swapchainDirty_ && !createSwapchain()) return nullptr;
Frame& f = frames_[frameIndex_]; Frame& f = frames_[frameIndex_];
@ -280,6 +299,10 @@ VkCommandBuffer Device::beginFrame() {
swapchainDirty_ = true; swapchainDirty_ = true;
return nullptr; return nullptr;
} }
if (r == VK_ERROR_SURFACE_LOST_KHR) {
surfaceLost_ = true;
return nullptr;
}
if (r != VK_SUBOPTIMAL_KHR) VK_CHECK(r); if (r != VK_SUBOPTIMAL_KHR) VK_CHECK(r);
VK_CHECK(vkResetFences(device_, 1, &f.inFlight)); VK_CHECK(vkResetFences(device_, 1, &f.inFlight));
@ -328,6 +351,7 @@ void Device::endFrame() {
} }
#endif #endif
if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR) swapchainDirty_ = true; 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); else VK_CHECK(r);
frameIndex_ = (frameIndex_ + 1) % kFramesInFlight; frameIndex_ = (frameIndex_ + 1) % kFramesInFlight;

View file

@ -50,6 +50,8 @@ public:
void setVsync(bool vsync) { vsync_ = vsync; swapchainDirty_ = true; } void setVsync(bool vsync) { vsync_ = vsync; swapchainDirty_ = true; }
void markResized() { 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_); } void waitIdle() { vkDeviceWaitIdle(device_); }
VkInstance instance() const { return instance_; } VkInstance instance() const { return instance_; }
@ -81,6 +83,7 @@ private:
}; };
bool createSwapchain(); bool createSwapchain();
bool recreateSurface();
void destroySwapchainViews(); void destroySwapchainViews();
const Window* window_ = nullptr; const Window* window_ = nullptr;
@ -102,7 +105,7 @@ private:
std::vector<VkImageView> views_; std::vector<VkImageView> views_;
std::vector<VkSemaphore> renderDone_; // per swapchain image, so present never reuses one in flight std::vector<VkSemaphore> renderDone_; // per swapchain image, so present never reuses one in flight
bool vsync_ = true; bool vsync_ = true;
bool swapchainDirty_ = false; bool swapchainDirty_ = false, surfaceLost_ = false;
Frame frames_[kFramesInFlight]; Frame frames_[kFramesInFlight];
VkCommandPool immPool_ = VK_NULL_HANDLE; VkCommandPool immPool_ = VK_NULL_HANDLE;

View file

@ -610,10 +610,51 @@ public:
loadPlugins(); loadPlugins();
if (!startPath_.empty()) openFile(startPath_); if (!startPath_.empty()) openFile(startPath_);
findAutosaves(); 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(); } void frame(double) override { autosave(); }
bool allowQuit() override { 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; if (!dirty_) return dropAutosave(), true;
askSave_ = 1; askSave_ = 1;
return false; 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) 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]); 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); App app(path);
return keel::run(app, int(rest.size()), rest.data()); return keel::run(app, int(rest.size()), rest.data());
} }