Skip to content

Commit

Permalink
Adding watch option using dmon (#1308)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mwestphal authored Mar 25, 2024
1 parent eb2d844 commit ec40b6e
Show file tree
Hide file tree
Showing 19 changed files with 2,020 additions and 39 deletions.
1 change: 1 addition & 0 deletions .cppcheck.supp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ unknownMacro
// external libraries
*:external/nlohmann_json/nlohmann/json.hpp
*:external/cxxopts/cxxopts.hpp
*:external/dmon/dmon.h

// specific checks
knownConditionTrueFalse:library/testing/TestSDKImage.cxx
Expand Down
1 change: 1 addition & 0 deletions .github/actions/coverage-ci/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ runs:
lcov --remove coverage.info "*/dependencies/*" -o coverage.info
lcov --remove coverage.info "*/cxxopts.hpp" -o coverage.info
lcov --remove coverage.info "*/json.hpp" -o coverage.info
lcov --remove coverage.info "*/dmon.h" -o coverage.info
lcov --remove coverage.info "*Test*" -o coverage.info
- name: Upload coverage to Codecov
Expand Down
4 changes: 4 additions & 0 deletions .tsan.supp
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ race:libvtkCommonDataModel

# OpenEXR
race:libOpenEXR

# dmon
# https://github.com/septag/dmon/issues/33
race:dmon.h
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,17 @@ option(F3D_MODULE_EXR "OpenEXR images module" OFF)
# Use externals
option(F3D_USE_EXTERNAL_CXXOPTS "Use external cxxopts dependency" OFF)
option(F3D_USE_EXTERNAL_NLOHMANN_JSON "Use external nlohmann_json dependency" OFF)
option(F3D_USE_EXTERNAL_DMON "Use external dmon dependency" OFF)

if (F3D_USE_EXTERNAL_CXXOPTS)
find_package(cxxopts REQUIRED)
endif ()
if (F3D_USE_EXTERNAL_NLOHMANN_JSON)
find_package(nlohmann_json REQUIRED)
endif ()
if (F3D_USE_EXTERNAL_DMON)
find_package(dmon REQUIRED)
endif ()

# VTK dependency
# Optional components should list VTK modules
Expand Down
7 changes: 7 additions & 0 deletions application/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ elseif (MSVC)
endif ()

target_include_directories(f3d PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>)

if (F3D_USE_EXTERNAL_CXXOPTS)
target_link_libraries(f3d PRIVATE cxxopts::cxxopts)
else ()
Expand All @@ -69,6 +70,12 @@ else ()
target_include_directories(f3d PUBLIC $<BUILD_INTERFACE:${F3D_SOURCE_DIR}/external/nlohmann_json>)
endif ()

if (F3D_USE_EXTERNAL_DMON)
target_link_libraries(f3d PRIVATE dmon::dmon)
else ()
target_include_directories(f3d PUBLIC $<BUILD_INTERFACE:${F3D_SOURCE_DIR}/external/dmon>)
endif ()

set(f3d_compile_options_private "")
set(f3d_compile_options_public "")
set(f3d_link_options_public "")
Expand Down
1 change: 1 addition & 0 deletions application/F3DOptionsParser.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ void ConfigurationOptions::GetOptions(F3DAppOptions& appOptions, f3d::options& o
this->DeclareOption(grp0, "dry-run", "", "Do not read the configuration file", appOptions.DryRun, HasDefault::YES, MayHaveConfig::NO );
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 );
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>");
this->DeclareOption(grp0, "watch", "", "Watch current file and automatically reload it whenever it is modified on disk", appOptions.Watch, HasDefault::YES, MayHaveConfig::YES );
this->DeclareOption(grp0, "load-plugins", "", "List of plugins to load separated with a comma", appOptions.Plugins, LocalHasDefaultNo, MayHaveConfig::YES, "<paths or names>");
this->DeclareOption(grp0, "scan-plugins", "", "Scan some directories for plugins (result can be incomplete)");

Expand Down
1 change: 1 addition & 0 deletions application/F3DOptionsParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ struct F3DAppOptions
std::string InteractionTestPlayFile = "";
bool NoBackground = false;
bool NoRender = false;
bool Watch = false;
double RefThreshold = 50;
double MaxSize = -1.0;

Expand Down
147 changes: 113 additions & 34 deletions application/F3DStarter.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,30 @@
#include "F3DOptionsParser.h"
#include "F3DSystemTools.h"

#define DMON_IMPL
#ifdef WIN32
#pragma warning(push)
#pragma warning(disable : 4505)
#include "dmon.h"
// dmon includes Windows.h which defines 'ERROR' and conflicts with log.h
#undef ERROR
#pragma warning(pop)
#else
#include "dmon.h"
#endif

#include "engine.h"
#include "interactor.h"
#include "log.h"
#include "options.h"
#include "window.h"

#include <algorithm>
#include <atomic>
#include <cassert>
#include <filesystem>
#include <iostream>
#include <mutex>
#include <set>

namespace fs = std::filesystem;
Expand Down Expand Up @@ -133,15 +147,35 @@ class F3DStarter::F3DInternals
}
}

static void dmonFolderChanged(
dmon_watch_id, dmon_action, const char*, const char* filename, const char*, void* userData)
{
F3DStarter* self = reinterpret_cast<F3DStarter*>(userData);
const std::lock_guard<std::mutex> lock(self->Internals->FilesListMutex);
fs::path filePath = self->Internals->FilesList[self->Internals->CurrentFileIndex];
if (filePath.filename().string() == std::string(filename))
{
self->Internals->ReloadFileRequested = true;
}
}

F3DOptionsParser Parser;
F3DAppOptions AppOptions;
f3d::options DynamicOptions;
f3d::options FileOptions;
std::unique_ptr<f3d::engine> Engine;
std::vector<fs::path> FilesList;
int CurrentFileIndex = -1;
dmon_watch_id FolderWatchId;
bool LoadedFile = false;
bool UpdateWithCommandLineParsing = true;

// dmon used atomic and mutex
std::atomic<int> CurrentFileIndex = -1;
std::mutex FilesListMutex;

// Event loop atomics
std::atomic<bool> RenderRequested = false;
std::atomic<bool> ReloadFileRequested = false;
};

//----------------------------------------------------------------------------
Expand All @@ -151,10 +185,17 @@ F3DStarter::F3DStarter()
// Set option outside of command line and config file
this->Internals->DynamicOptions.set(
"ui.dropzone-info", "Drop a file or HDRI to load it\nPress H to show cheatsheet");

// Initialize dmon
dmon_init();
}

//----------------------------------------------------------------------------
F3DStarter::~F3DStarter() = default;
F3DStarter::~F3DStarter()
{
// deinit dmon
dmon_deinit();
}

//----------------------------------------------------------------------------
int F3DStarter::Start(int argc, char** argv)
Expand Down Expand Up @@ -219,41 +260,17 @@ int F3DStarter::Start(int argc, char** argv)
interactor.setKeyPressCallBack(
[this](int, const std::string& keySym) -> bool
{
const auto loadFile = [this](int index, bool restoreCamera = false) -> bool
{
this->Internals->Engine->getInteractor().stopAnimation();

f3d::log::debug("========== Loading 3D file ==========");

if (restoreCamera)
{
f3d::camera& cam = this->Internals->Engine->getWindow().getCamera();
const auto camState = cam.getState();
this->LoadFile(index, true);
cam.setState(camState);
}
else
{
this->LoadFile(index, true);
}

f3d::log::debug("========== Rendering ==========");

this->Render();
return true;
};

if (keySym == "Left")
{
return loadFile(-1);
return this->LoadRelativeFile(-1);
}
if (keySym == "Right")
{
return loadFile(+1);
return this->LoadRelativeFile(+1);
}
if (keySym == "Up")
{
return loadFile(0, true);
return this->LoadRelativeFile(0, true);
}
if (keySym == "Down")
{
Expand All @@ -264,7 +281,7 @@ int F3DStarter::Start(int argc, char** argv)
this->Internals->FilesList[static_cast<size_t>(this->Internals->CurrentFileIndex)]
.parent_path(),
true);
return loadFile(0);
return this->LoadRelativeFile(0);
}
return true;
}
Expand Down Expand Up @@ -299,7 +316,7 @@ int F3DStarter::Start(int argc, char** argv)
{
this->LoadFile(index);
}
this->Render();
this->RequestRender();
return true;
});
window
Expand Down Expand Up @@ -337,7 +354,6 @@ int F3DStarter::Start(int argc, char** argv)

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

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

if (!this->Internals->LoadedFile)
if (this->Internals->LoadedFile)
{
if (this->Internals->AppOptions.Watch)
{
// Always unwatch and watch current folder, even on reload
if (this->Internals->FolderWatchId.id > 0)
{
dmon_unwatch(this->Internals->FolderWatchId);
}
this->Internals->FolderWatchId = dmon_watch(
filePath.parent_path().string().c_str(), &F3DInternals::dmonFolderChanged, 0, this);
}
}
else
{
// No file loaded, remove any previously loaded file
loader.loadGeometry("", true);
Expand All @@ -631,9 +663,17 @@ void F3DStarter::LoadFile(int index, bool relativeIndex)
this->Internals->Engine->getOptions().set("ui.filename-info", filenameInfo);
}

//----------------------------------------------------------------------------
void F3DStarter::RequestRender()
{
// Render will be called by the next event loop
this->Internals->RenderRequested = true;
}

//----------------------------------------------------------------------------
void F3DStarter::Render()
{
f3d::log::debug("========== Rendering ==========");
this->Internals->Engine->getWindow().render();
f3d::log::debug("Render done");
}
Expand Down Expand Up @@ -671,6 +711,8 @@ int F3DStarter::AddFile(const fs::path& path, bool quiet)

if (it == this->Internals->FilesList.end())
{
// In the main thread, we only need to guard writing
const std::lock_guard<std::mutex> lock(this->Internals->FilesListMutex);
this->Internals->FilesList.push_back(tmpPath);
return static_cast<int>(this->Internals->FilesList.size()) - 1;
}
Expand All @@ -684,3 +726,40 @@ int F3DStarter::AddFile(const fs::path& path, bool quiet)
}
}
}

//----------------------------------------------------------------------------
bool F3DStarter::LoadRelativeFile(int index, bool restoreCamera)
{
this->Internals->Engine->getInteractor().stopAnimation();

if (restoreCamera)
{
f3d::camera& cam = this->Internals->Engine->getWindow().getCamera();
const auto camState = cam.getState();
this->LoadFile(index, true);
cam.setState(camState);
}
else
{
this->LoadFile(index, true);
}

this->RequestRender();

return true;
}

//----------------------------------------------------------------------------
void F3DStarter::EventLoop()
{
if (this->Internals->ReloadFileRequested)
{
this->LoadRelativeFile(0, true);
this->Internals->ReloadFileRequested = false;
}
if (this->Internals->RenderRequested)
{
this->Render();
this->Internals->RenderRequested = false;
}
}
20 changes: 19 additions & 1 deletion application/F3DStarter.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ class F3DStarter
void LoadFile(int index = 0, bool relativeIndex = false);

/**
* Trigger a render
* Trigger a render on the next event loop
*/
void RequestRender();

/**
* Trigger a render immediately (must be called by the main thread)
*/
void Render();

Expand All @@ -47,6 +52,19 @@ class F3DStarter
private:
class F3DInternals;
std::unique_ptr<F3DInternals> Internals;

/**
* Internal method triggered when interacting with the application
* that load a file using relative index and handle camera restore
*/
bool LoadRelativeFile(int relativeIndex = 0, bool restoreCamera = false);

/**
* Internal event loop that is triggered repeatedly to handle specific events:
* - Render
* - ReloadFile
*/
void EventLoop();
};

#endif
Loading

0 comments on commit ec40b6e

Please sign in to comment.