Skip to content

Commit ec40b6e

Browse files
authored
Adding watch option using dmon (f3d-app#1308)
This add support for an "--watch" cli option where the current file is reloaded automatically if changed on disk. It relies on https://github.com/septag/dmon for detecting the changes. In order to support this properly, F3DStarter is reworked with a proper event loops that currently supports: - Reloading the current file - Rendering This also adds a test for it on linux/apple/windows
1 parent eb2d844 commit ec40b6e

19 files changed

+2020
-39
lines changed

.cppcheck.supp

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ unknownMacro
66
// external libraries
77
*:external/nlohmann_json/nlohmann/json.hpp
88
*:external/cxxopts/cxxopts.hpp
9+
*:external/dmon/dmon.h
910

1011
// specific checks
1112
knownConditionTrueFalse:library/testing/TestSDKImage.cxx

.github/actions/coverage-ci/action.yml

+1
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ runs:
102102
lcov --remove coverage.info "*/dependencies/*" -o coverage.info
103103
lcov --remove coverage.info "*/cxxopts.hpp" -o coverage.info
104104
lcov --remove coverage.info "*/json.hpp" -o coverage.info
105+
lcov --remove coverage.info "*/dmon.h" -o coverage.info
105106
lcov --remove coverage.info "*Test*" -o coverage.info
106107
107108
- name: Upload coverage to Codecov

.tsan.supp

+4
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ race:libvtkCommonDataModel
1010

1111
# OpenEXR
1212
race:libOpenEXR
13+
14+
# dmon
15+
# https://github.com/septag/dmon/issues/33
16+
race:dmon.h

CMakeLists.txt

+4
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,17 @@ option(F3D_MODULE_EXR "OpenEXR images module" OFF)
6060
# Use externals
6161
option(F3D_USE_EXTERNAL_CXXOPTS "Use external cxxopts dependency" OFF)
6262
option(F3D_USE_EXTERNAL_NLOHMANN_JSON "Use external nlohmann_json dependency" OFF)
63+
option(F3D_USE_EXTERNAL_DMON "Use external dmon dependency" OFF)
6364

6465
if (F3D_USE_EXTERNAL_CXXOPTS)
6566
find_package(cxxopts REQUIRED)
6667
endif ()
6768
if (F3D_USE_EXTERNAL_NLOHMANN_JSON)
6869
find_package(nlohmann_json REQUIRED)
6970
endif ()
71+
if (F3D_USE_EXTERNAL_DMON)
72+
find_package(dmon REQUIRED)
73+
endif ()
7074

7175
# VTK dependency
7276
# Optional components should list VTK modules

application/CMakeLists.txt

+7
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ elseif (MSVC)
5757
endif ()
5858

5959
target_include_directories(f3d PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>)
60+
6061
if (F3D_USE_EXTERNAL_CXXOPTS)
6162
target_link_libraries(f3d PRIVATE cxxopts::cxxopts)
6263
else ()
@@ -69,6 +70,12 @@ else ()
6970
target_include_directories(f3d PUBLIC $<BUILD_INTERFACE:${F3D_SOURCE_DIR}/external/nlohmann_json>)
7071
endif ()
7172

73+
if (F3D_USE_EXTERNAL_DMON)
74+
target_link_libraries(f3d PRIVATE dmon::dmon)
75+
else ()
76+
target_include_directories(f3d PUBLIC $<BUILD_INTERFACE:${F3D_SOURCE_DIR}/external/dmon>)
77+
endif ()
78+
7279
set(f3d_compile_options_private "")
7380
set(f3d_compile_options_public "")
7481
set(f3d_link_options_public "")

application/F3DOptionsParser.cxx

+1
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ void ConfigurationOptions::GetOptions(F3DAppOptions& appOptions, f3d::options& o
311311
this->DeclareOption(grp0, "dry-run", "", "Do not read the configuration file", appOptions.DryRun, HasDefault::YES, MayHaveConfig::NO );
312312
this->DeclareOption(grp0, "no-render", "", "Do not render anything and quit right after loading the first file, use with --verbose to recover information about a file.", appOptions.NoRender, HasDefault::YES, MayHaveConfig::YES );
313313
this->DeclareOption(grp0, "max-size", "", "Maximum size in Mib of a file to load, negative value means unlimited", appOptions.MaxSize, HasDefault::YES, MayHaveConfig::YES, "<size in Mib>");
314+
this->DeclareOption(grp0, "watch", "", "Watch current file and automatically reload it whenever it is modified on disk", appOptions.Watch, HasDefault::YES, MayHaveConfig::YES );
314315
this->DeclareOption(grp0, "load-plugins", "", "List of plugins to load separated with a comma", appOptions.Plugins, LocalHasDefaultNo, MayHaveConfig::YES, "<paths or names>");
315316
this->DeclareOption(grp0, "scan-plugins", "", "Scan some directories for plugins (result can be incomplete)");
316317

application/F3DOptionsParser.h

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ struct F3DAppOptions
2929
std::string InteractionTestPlayFile = "";
3030
bool NoBackground = false;
3131
bool NoRender = false;
32+
bool Watch = false;
3233
double RefThreshold = 50;
3334
double MaxSize = -1.0;
3435

application/F3DStarter.cxx

+113-34
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,30 @@
66
#include "F3DOptionsParser.h"
77
#include "F3DSystemTools.h"
88

9+
#define DMON_IMPL
10+
#ifdef WIN32
11+
#pragma warning(push)
12+
#pragma warning(disable : 4505)
13+
#include "dmon.h"
14+
// dmon includes Windows.h which defines 'ERROR' and conflicts with log.h
15+
#undef ERROR
16+
#pragma warning(pop)
17+
#else
18+
#include "dmon.h"
19+
#endif
20+
921
#include "engine.h"
1022
#include "interactor.h"
1123
#include "log.h"
1224
#include "options.h"
1325
#include "window.h"
1426

1527
#include <algorithm>
28+
#include <atomic>
1629
#include <cassert>
1730
#include <filesystem>
1831
#include <iostream>
32+
#include <mutex>
1933
#include <set>
2034

2135
namespace fs = std::filesystem;
@@ -133,15 +147,35 @@ class F3DStarter::F3DInternals
133147
}
134148
}
135149

150+
static void dmonFolderChanged(
151+
dmon_watch_id, dmon_action, const char*, const char* filename, const char*, void* userData)
152+
{
153+
F3DStarter* self = reinterpret_cast<F3DStarter*>(userData);
154+
const std::lock_guard<std::mutex> lock(self->Internals->FilesListMutex);
155+
fs::path filePath = self->Internals->FilesList[self->Internals->CurrentFileIndex];
156+
if (filePath.filename().string() == std::string(filename))
157+
{
158+
self->Internals->ReloadFileRequested = true;
159+
}
160+
}
161+
136162
F3DOptionsParser Parser;
137163
F3DAppOptions AppOptions;
138164
f3d::options DynamicOptions;
139165
f3d::options FileOptions;
140166
std::unique_ptr<f3d::engine> Engine;
141167
std::vector<fs::path> FilesList;
142-
int CurrentFileIndex = -1;
168+
dmon_watch_id FolderWatchId;
143169
bool LoadedFile = false;
144170
bool UpdateWithCommandLineParsing = true;
171+
172+
// dmon used atomic and mutex
173+
std::atomic<int> CurrentFileIndex = -1;
174+
std::mutex FilesListMutex;
175+
176+
// Event loop atomics
177+
std::atomic<bool> RenderRequested = false;
178+
std::atomic<bool> ReloadFileRequested = false;
145179
};
146180

147181
//----------------------------------------------------------------------------
@@ -151,10 +185,17 @@ F3DStarter::F3DStarter()
151185
// Set option outside of command line and config file
152186
this->Internals->DynamicOptions.set(
153187
"ui.dropzone-info", "Drop a file or HDRI to load it\nPress H to show cheatsheet");
188+
189+
// Initialize dmon
190+
dmon_init();
154191
}
155192

156193
//----------------------------------------------------------------------------
157-
F3DStarter::~F3DStarter() = default;
194+
F3DStarter::~F3DStarter()
195+
{
196+
// deinit dmon
197+
dmon_deinit();
198+
}
158199

159200
//----------------------------------------------------------------------------
160201
int F3DStarter::Start(int argc, char** argv)
@@ -219,41 +260,17 @@ int F3DStarter::Start(int argc, char** argv)
219260
interactor.setKeyPressCallBack(
220261
[this](int, const std::string& keySym) -> bool
221262
{
222-
const auto loadFile = [this](int index, bool restoreCamera = false) -> bool
223-
{
224-
this->Internals->Engine->getInteractor().stopAnimation();
225-
226-
f3d::log::debug("========== Loading 3D file ==========");
227-
228-
if (restoreCamera)
229-
{
230-
f3d::camera& cam = this->Internals->Engine->getWindow().getCamera();
231-
const auto camState = cam.getState();
232-
this->LoadFile(index, true);
233-
cam.setState(camState);
234-
}
235-
else
236-
{
237-
this->LoadFile(index, true);
238-
}
239-
240-
f3d::log::debug("========== Rendering ==========");
241-
242-
this->Render();
243-
return true;
244-
};
245-
246263
if (keySym == "Left")
247264
{
248-
return loadFile(-1);
265+
return this->LoadRelativeFile(-1);
249266
}
250267
if (keySym == "Right")
251268
{
252-
return loadFile(+1);
269+
return this->LoadRelativeFile(+1);
253270
}
254271
if (keySym == "Up")
255272
{
256-
return loadFile(0, true);
273+
return this->LoadRelativeFile(0, true);
257274
}
258275
if (keySym == "Down")
259276
{
@@ -264,7 +281,7 @@ int F3DStarter::Start(int argc, char** argv)
264281
this->Internals->FilesList[static_cast<size_t>(this->Internals->CurrentFileIndex)]
265282
.parent_path(),
266283
true);
267-
return loadFile(0);
284+
return this->LoadRelativeFile(0);
268285
}
269286
return true;
270287
}
@@ -299,7 +316,7 @@ int F3DStarter::Start(int argc, char** argv)
299316
{
300317
this->LoadFile(index);
301318
}
302-
this->Render();
319+
this->RequestRender();
303320
return true;
304321
});
305322
window
@@ -337,7 +354,6 @@ int F3DStarter::Start(int argc, char** argv)
337354

338355
if (!this->Internals->AppOptions.NoRender)
339356
{
340-
f3d::log::debug("========== Rendering ==========");
341357
f3d::window& window = this->Internals->Engine->getWindow();
342358
f3d::interactor& interactor = this->Internals->Engine->getInteractor();
343359

@@ -460,7 +476,9 @@ int F3DStarter::Start(int argc, char** argv)
460476
f3d::log::error("This is a headless build of F3D, interactive rendering is not supported");
461477
return EXIT_FAILURE;
462478
#else
463-
this->Render();
479+
// Create the event loop repeating timer
480+
interactor.createTimerCallBack(30, [this]() { this->EventLoop(); });
481+
this->RequestRender();
464482
interactor.start();
465483
#endif
466484
}
@@ -472,6 +490,7 @@ int F3DStarter::Start(int argc, char** argv)
472490
//----------------------------------------------------------------------------
473491
void F3DStarter::LoadFile(int index, bool relativeIndex)
474492
{
493+
f3d::log::debug("========== Loading 3D file ==========");
475494
// When loading a file, store any changed options
476495
// into the dynamic options and use these dynamic option as the default
477496
// for loading the file while still applying file specific options on top of it
@@ -621,7 +640,20 @@ void F3DStarter::LoadFile(int index, bool relativeIndex)
621640
}
622641
}
623642

624-
if (!this->Internals->LoadedFile)
643+
if (this->Internals->LoadedFile)
644+
{
645+
if (this->Internals->AppOptions.Watch)
646+
{
647+
// Always unwatch and watch current folder, even on reload
648+
if (this->Internals->FolderWatchId.id > 0)
649+
{
650+
dmon_unwatch(this->Internals->FolderWatchId);
651+
}
652+
this->Internals->FolderWatchId = dmon_watch(
653+
filePath.parent_path().string().c_str(), &F3DInternals::dmonFolderChanged, 0, this);
654+
}
655+
}
656+
else
625657
{
626658
// No file loaded, remove any previously loaded file
627659
loader.loadGeometry("", true);
@@ -631,9 +663,17 @@ void F3DStarter::LoadFile(int index, bool relativeIndex)
631663
this->Internals->Engine->getOptions().set("ui.filename-info", filenameInfo);
632664
}
633665

666+
//----------------------------------------------------------------------------
667+
void F3DStarter::RequestRender()
668+
{
669+
// Render will be called by the next event loop
670+
this->Internals->RenderRequested = true;
671+
}
672+
634673
//----------------------------------------------------------------------------
635674
void F3DStarter::Render()
636675
{
676+
f3d::log::debug("========== Rendering ==========");
637677
this->Internals->Engine->getWindow().render();
638678
f3d::log::debug("Render done");
639679
}
@@ -671,6 +711,8 @@ int F3DStarter::AddFile(const fs::path& path, bool quiet)
671711

672712
if (it == this->Internals->FilesList.end())
673713
{
714+
// In the main thread, we only need to guard writing
715+
const std::lock_guard<std::mutex> lock(this->Internals->FilesListMutex);
674716
this->Internals->FilesList.push_back(tmpPath);
675717
return static_cast<int>(this->Internals->FilesList.size()) - 1;
676718
}
@@ -684,3 +726,40 @@ int F3DStarter::AddFile(const fs::path& path, bool quiet)
684726
}
685727
}
686728
}
729+
730+
//----------------------------------------------------------------------------
731+
bool F3DStarter::LoadRelativeFile(int index, bool restoreCamera)
732+
{
733+
this->Internals->Engine->getInteractor().stopAnimation();
734+
735+
if (restoreCamera)
736+
{
737+
f3d::camera& cam = this->Internals->Engine->getWindow().getCamera();
738+
const auto camState = cam.getState();
739+
this->LoadFile(index, true);
740+
cam.setState(camState);
741+
}
742+
else
743+
{
744+
this->LoadFile(index, true);
745+
}
746+
747+
this->RequestRender();
748+
749+
return true;
750+
}
751+
752+
//----------------------------------------------------------------------------
753+
void F3DStarter::EventLoop()
754+
{
755+
if (this->Internals->ReloadFileRequested)
756+
{
757+
this->LoadRelativeFile(0, true);
758+
this->Internals->ReloadFileRequested = false;
759+
}
760+
if (this->Internals->RenderRequested)
761+
{
762+
this->Render();
763+
this->Internals->RenderRequested = false;
764+
}
765+
}

application/F3DStarter.h

+19-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ class F3DStarter
3434
void LoadFile(int index = 0, bool relativeIndex = false);
3535

3636
/**
37-
* Trigger a render
37+
* Trigger a render on the next event loop
38+
*/
39+
void RequestRender();
40+
41+
/**
42+
* Trigger a render immediately (must be called by the main thread)
3843
*/
3944
void Render();
4045

@@ -47,6 +52,19 @@ class F3DStarter
4752
private:
4853
class F3DInternals;
4954
std::unique_ptr<F3DInternals> Internals;
55+
56+
/**
57+
* Internal method triggered when interacting with the application
58+
* that load a file using relative index and handle camera restore
59+
*/
60+
bool LoadRelativeFile(int relativeIndex = 0, bool restoreCamera = false);
61+
62+
/**
63+
* Internal event loop that is triggered repeatedly to handle specific events:
64+
* - Render
65+
* - ReloadFile
66+
*/
67+
void EventLoop();
5068
};
5169

5270
#endif

0 commit comments

Comments
 (0)