Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/about/Authors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Harlan Playford playfordh
Hayati Ayguen hayguen
Herwig Hochleitner bendlas
Hevlikn Hevlikn
Huitzilopochtli-98 Huitzilopochtli-98
Ian S kremlin-
IndigoFenix
Jacek Konieczny Jajcus
Expand Down
2 changes: 2 additions & 0 deletions docs/changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Template for new versions:
# Future

## New Tools
- `worldmap-zoom-render`: add a Windows SDL2 native composition service for 2x and 4x high-level world-map zoom, preserving interface scale

## New Features
- `autofarm`: farm plots can now be individually excluded from autofarm management via new controls on the farm plot information sheet, which also warn when a farm's crops are being managed by autofarm
Expand All @@ -77,6 +78,7 @@ Template for new versions:
## Documentation

## API
- ``queueOnRenderThread``: cancellable render-task handles report completion after callback/capture destruction, allowing unloadable plugins to drain render work safely

## Lua
- ``dfhack.timeout_until``: new function to schedule a callback to run on a specified in-game tick
Expand Down
39 changes: 39 additions & 0 deletions docs/dev/CoreSuspender.rst
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,45 @@ Caveats and pitfalls
frame-length wait. Grouping operations into one critical section is still
good advice for latency, not correctness.

.. _render-thread-tasks:

Cancellable render-thread work and plugin unload
------------------------------------------------

``runOnRenderThread(callback)`` queues a fire-and-forget ``std::function``.
Its execution and subsequent destruction happen in the core render queue.
An unloadable plugin must account for the callback's function manager and the
destructors of any captures, as well as for its callback body. A completion flag
written inside the callback body can become visible *before* those destructors
have run.

``queueOnRenderThread(callback)`` (``RenderThreadTask.h``, also included by
``modules/DFSDL.h``) returns a shared ``RenderThreadTask`` handle:

- ``cancel()`` cancels a pending task and destroys its callback/captures before
returning true. It returns false if execution/cancellation already started.
- ``finished()`` reports that the callable and its captures have been destroyed,
after either execution or cancellation. A cancelled task's empty queue wrapper
can remain queued: that wrapper and its state destructor are compiled in core.
- ``succeeded()`` is true only after a callback returned normally and its
captures were destroyed. Cancelled or throwing callbacks report false. The
task catches exceptions without retaining an ``exception_ptr``, whose payload
could itself have a destructor in the caller's module.
- ``waitFor(std::chrono::milliseconds)`` waits for ``finished()`` with a timeout.
It is forbidden on the render thread or while holding a core suspend context;
use polling on later simulation frames in those contexts. A timeout gives no
permission to unload the callback's module.

The handles track queued work only. Plugins must separately remove every native
hook/event callback on its owning thread, stop new work from being submitted,
and establish completion before permitting their module to unload. Store task
handles until then; dropping a handle alone does not cancel a queued callback.

The world-map renderer's methodology documentation describes a concrete
application: its console unload waits without the core lock, its Lua owner can
request nonblocking cleanup and poll, and its process-shutdown path cleans up
inline on the render thread.

Reference
---------

Expand Down
328 changes: 328 additions & 0 deletions docs/plugins/worldmap-zoom-render.rst

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions library/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ set(MAIN_HEADERS
include/RemoteClient.h
include/RemoteServer.h
include/RemoteTools.h
include/RenderThreadTask.h
include/Signal.hpp
include/TileTypes.h
include/Types.h
Expand Down Expand Up @@ -117,13 +118,25 @@ set(MAIN_SOURCES
RemoteClient.cpp
RemoteServer.cpp
RemoteTools.cpp
RenderThreadTask.cpp
)

file(GLOB_RECURSE TEST_SOURCES
LIST_DIRECTORIES false
*test.cpp)
dfhack_test(dfhack-test "${TEST_SOURCES}")

if(BUILD_TESTING)
find_package(Threads REQUIRED)
# Standalone: test the production task implementation with an injected
# transport, without requiring a game, SDL context, or a loaded DFHack core.
add_executable(render-thread-task-test tests/render-thread-task.cpp RenderThreadTask.cpp)
target_include_directories(render-thread-task-test PRIVATE include)
target_compile_definitions(render-thread-task-test PRIVATE DFHACK_EXPORT= DFHACK_IMPORT=)
target_link_libraries(render-thread-task-test PRIVATE Threads::Threads)
add_test(NAME render-thread-task-test COMMAND render-thread-task-test)
endif()

if(WIN32)
set(CONSOLE_SOURCES Console-windows.cpp)
else()
Expand Down
96 changes: 96 additions & 0 deletions library/RenderThreadTask.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SPDX-License-Identifier: Zlib
#include <condition_variable>
#include <mutex>
#include <utility>

#include "RenderThreadTask.h"

namespace DFHack {

struct RenderThreadTask::State {
enum class Phase { pending, running, cancelling, finished };
mutable std::mutex mutex;
std::condition_variable changed;
std::function<void()> callback;
Phase phase = Phase::pending;
bool succeeded = false;
};

RenderThreadTask::RenderThreadTask(std::function<void()> callback) : state(std::make_unique<State>()) {
state->callback = std::move(callback);
}

RenderThreadTask::~RenderThreadTask() = default;

bool RenderThreadTask::cancel() {
std::function<void()> discarded;
{
std::lock_guard lock(state->mutex);
if (state->phase != State::Phase::pending)
return false;
state->phase = State::Phase::cancelling;
discarded = std::move(state->callback);
state->callback = {};
}
// Captures can have nontrivial destructors. Destroy them outside the lock,
// and do not publish completion while any plugin-owned destructor remains.
discarded = {};
{
std::lock_guard lock(state->mutex);
state->phase = State::Phase::finished;
}
state->changed.notify_all();
return true;
}

void RenderThreadTask::run() {
std::function<void()> callback;
{
std::lock_guard lock(state->mutex);
if (state->phase != State::Phase::pending)
return;
state->phase = State::Phase::running;
callback = std::move(state->callback);
state->callback = {};
}
bool success = false;
try {
callback();
success = true;
} catch (...) {
// Do not retain an exception_ptr: an exception's destructor might also
// reside in the plugin. The caller can inspect succeeded() instead.
}
callback = {};
{
std::lock_guard lock(state->mutex);
state->succeeded = success;
state->phase = State::Phase::finished;
}
state->changed.notify_all();
}

bool RenderThreadTask::finished() const {
std::lock_guard lock(state->mutex);
return state->phase == State::Phase::finished;
}

bool RenderThreadTask::succeeded() const {
std::lock_guard lock(state->mutex);
return state->phase == State::Phase::finished && state->succeeded;
}

bool RenderThreadTask::waitFor(std::chrono::milliseconds timeout) const {
std::unique_lock lock(state->mutex);
return state->changed.wait_for(lock, timeout, [this] { return state->phase == State::Phase::finished; });
}

std::shared_ptr<RenderThreadTask> queueOnRenderThread(std::function<void()> callback) {
auto task = std::shared_ptr<RenderThreadTask>(new RenderThreadTask(std::move(callback)));
// This wrapper's std::function manager is compiled into core, not into the
// caller's module. A cancelled/finished task can safely remain in the queue.
runOnRenderThread([task] { task->run(); });
return task;
}

} // namespace DFHack
40 changes: 40 additions & 0 deletions library/include/RenderThreadTask.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: Zlib
#pragma once

#include <chrono>
#include <functional>
#include <memory>

#include "Export.h"

namespace DFHack {

// Fire-and-forget work must not refer to an unloadable plugin. Use the task
// handle below when callback code or captured objects belong to a plugin.
DFHACK_EXPORT void runOnRenderThread(std::function<void()> callback);

class DFHACK_EXPORT RenderThreadTask;
DFHACK_EXPORT std::shared_ptr<RenderThreadTask> queueOnRenderThread(std::function<void()> callback);

// The implementation, queue wrapper, and callback destruction live in core.
// A finished task contains no callable or capture whose code lives in a plugin.
class DFHACK_EXPORT RenderThreadTask {
public:
~RenderThreadTask();
bool cancel();
bool finished() const;
bool succeeded() const;
// Never wait on the render thread or while holding a core suspend context:
// the render thread may itself be waiting for simulation to advance.
bool waitFor(std::chrono::milliseconds timeout) const;

private:
struct State;
std::unique_ptr<State> state;
explicit RenderThreadTask(std::function<void()> callback);
void run();
friend DFHACK_EXPORT std::shared_ptr<RenderThreadTask>
queueOnRenderThread(std::function<void()> callback);
};

} // namespace DFHack
5 changes: 3 additions & 2 deletions library/include/modules/DFSDL.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "ColorText.h"
#include "Export.h"
#include "PluginManager.h"
#include "RenderThreadTask.h"

#include <cstdint>
#include <functional>
Expand Down Expand Up @@ -84,7 +85,7 @@ namespace DFHack
DFHACK_EXPORT bool getClipboardTextCp437Multiline(std::vector<std::string> * lines);
DFHACK_EXPORT bool setClipboardTextCp437Multiline(std::string text);

// Queue a cb to be run on the render thread
DFHACK_EXPORT void runOnRenderThread(std::function<void()> cb);
// runOnRenderThread() and the cancellable queueOnRenderThread() task API
// are declared in RenderThreadTask.h.
DFHACK_EXPORT void runRenderThreadCallbacks();
}
Loading