changeset 1386:dfb48f0794b1

Ongoing splitting SDL vs WASM (preparing RtViewer WASM)
author Benjamin Golinvaux <bgo@osimis.io>
date Mon, 27 Apr 2020 16:48:19 +0200
parents ffe9beb7c5d3
children 4ebf246f3919
files Samples/Common/RtViewer.cpp Samples/Common/RtViewer.h Samples/Common/SampleHelpers.h Samples/Sdl/RtViewer/CMakeLists.txt Samples/Sdl/RtViewer/RtViewerSdl.cpp Samples/Sdl/SdlHelpers.h
diffstat 6 files changed, 417 insertions(+), 247 deletions(-) [+]
line wrap: on
line diff
--- a/Samples/Common/RtViewer.cpp	Mon Apr 27 16:47:46 2020 +0200
+++ b/Samples/Common/RtViewer.cpp	Mon Apr 27 16:48:19 2020 +0200
@@ -18,23 +18,12 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
+// Sample app
 #include "RtViewer.h"
 
-#include <stdio.h>
-
-#include <boost/shared_ptr.hpp>
-#include <boost/weak_ptr.hpp>
-#include <boost/make_shared.hpp>
-
-#include <Core/Images/Image.h>
-#include <Core/Images/ImageProcessing.h>
-#include <Core/Images/PngWriter.h>
-#include <Core/Logging.h>
-#include <Core/OrthancException.h>
-
+// Stone of Orthanc
 #include <Framework/OpenGL/SdlOpenGLContext.h>
 #include <Framework/StoneInitialization.h>
-
 #include <Framework/Scene2D/CairoCompositor.h>
 #include <Framework/Scene2D/ColorTextureSceneLayer.h>
 #include <Framework/Scene2D/OpenGLCompositor.h>
@@ -57,6 +46,21 @@
 #include <Framework/Volumes/DicomVolumeImageMPRSlicer.h>
 #include <Framework/StoneException.h>
 
+// Orthanc
+#include <Core/Images/Image.h>
+#include <Core/Images/ImageProcessing.h>
+#include <Core/Images/PngWriter.h>
+#include <Core/Logging.h>
+#include <Core/OrthancException.h>
+
+// System 
+#include <boost/shared_ptr.hpp>
+#include <boost/weak_ptr.hpp>
+#include <boost/make_shared.hpp>
+
+#include <stdio.h>
+
+
 namespace OrthancStone
 {
   const char* RtViewerGuiToolToString(size_t i)
@@ -263,6 +267,18 @@
         }
         break;
 
+      case SDLK_r:
+        UpdateLayers();
+        {
+          std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+          lock->Invalidate();
+        }
+        break;
+
+      case SDLK_s:
+        compositor.FitContent(scene);
+        break;
+
       case SDLK_t:
         if (!activeTracker_)
           SelectNextTool();
@@ -272,9 +288,6 @@
             " is taking place";
         }
         break;
-      case SDLK_s:
-        compositor.FitContent(scene);
-        break;
 
       case SDLK_z:
         LOG(TRACE) << "SDLK_z has been pressed. event.key.keysym.mod == " << event.key.keysym.mod;
@@ -466,15 +479,11 @@
 
 
   RtViewerApp::RtViewerApp()
-    : oracle_(*this)
-    , currentTool_(RtViewerGuiTool_Rotate)
+    : currentTool_(RtViewerGuiTool_Rotate)
     , undoStack_(new UndoStack)
     , currentPlane_(0)
     , projection_(VolumeProjection_Coronal)
   {
-    loadersContext_.reset(new GenericLoadersContext(1, 4, 1));
-    loadersContext_->StartOracle();
-
     // False means we do NOT let Windows treat this as a legacy application that needs to be scaled
     viewport_ = SdlOpenGLViewport::Create("CT RTDOSE RTSTRUCT viewer", 1024, 1024, false);
 
@@ -482,18 +491,9 @@
     ViewportController& controller = lock->GetController();
     Scene2D& scene = controller.GetScene();
 
-    //oracleObservable.RegisterObserverCallback
-    //(new Callable
-    //  <RtViewerApp, SleepOracleCommand::TimeoutMessage>(*this, &RtViewerApp::Handle));
-
-    //oracleObservable.RegisterObserverCallback
-    //(new Callable
-    //  <Toto, GetOrthancImageCommand::SuccessMessage>(*this, &RtViewerApp::Handle));
-
-    //oracleObservable.RegisterObserverCallback
-    //(new Callable
-    //  <RtViewerApp, GetOrthancWebViewerJpegCommand::SuccessMessage>(*this, &ToRtViewerAppto::Handle));
-
+    // Create the volumes that will be filled later on
+    ctVolume_ = boost::make_shared<DicomVolumeImage>();
+    doseVolume_ = boost::make_shared<DicomVolumeImage>();
 
     TEXTURE_2x2_1_ZINDEX = 1;
     TEXTURE_1x1_ZINDEX = 2;
@@ -509,7 +509,6 @@
     std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
     ViewportController& controller = lock->GetController();
     Scene2D& scene = controller.GetScene();
-    Register<OracleCommandExceptionMessage>(oracleObservable_, &RtViewerApp::Handle);
     Register<ViewportController::SceneTransformChanged>(controller, &RtViewerApp::OnSceneTransformChanged);
   }
 
@@ -520,6 +519,7 @@
     return thisOne;
   }
 
+#if 0
   void RtViewerApp::PrepareScene()
   {
     std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
@@ -551,6 +551,7 @@
       scene.SetLayer(TEXTURE_2x2_1_ZINDEX, new ColorTextureSceneLayer(i));
     }
   }
+#endif
 
   void RtViewerApp::DisableTracker()
   {
@@ -588,33 +589,107 @@
     // std::vector<boost::shared_ptr<MeasureTool>> measureTools_;
     return boost::shared_ptr<IFlexiblePointerTracker>();
   }
+    
+  void RtViewerApp::PrepareLoadersAndSlicers()
+  {
 
-  static void GLAPIENTRY
-    OpenGLMessageCallback(GLenum source,
-                          GLenum type,
-                          GLuint id,
-                          GLenum severity,
-                          GLsizei length,
-                          const GLchar* message,
-                          const void* userParam)
-  {
-    if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
+    //{
+    //  Orthanc::WebServiceParameters p;
+    //  //p.SetUrl("http://localhost:8043/");
+    //  p.SetCredentials("orthanc", "orthanc");
+    //  oracle_.SetOrthancParameters(p);
+    //}
+
     {
-      fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
-              (type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""),
-              type, severity, message);
+      // "true" means use progressive quality (jpeg 50 --> jpeg 90 --> 16-bit raw)
+      // "false" means only using hi quality
+      // TODO: add flag for quality
+      ctLoader_ = OrthancSeriesVolumeProgressiveLoader::Create(*loadersContext_, ctVolume_, false);
+
+      // we need to store the CT loader to ask from geometry details later on when geometry is loaded
+      geometryProvider_ = ctLoader_;
+
+      doseLoader_ = OrthancMultiframeVolumeLoader::Create(*loadersContext_, doseVolume_);
+      rtstructLoader_ = DicomStructureSetLoader::Create(*loadersContext_);
     }
+
+    /**
+    Register for notifications issued by the loaders
+    */
+
+    Register<DicomVolumeImage::GeometryReadyMessage>                              
+       (*ctLoader_, &RtViewerApp::HandleGeometryReady);
+    
+    Register<OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality>
+      (*ctLoader_, &RtViewerApp::HandleCTLoaded);
+    
+    Register<DicomVolumeImage::ContentUpdatedMessage>                             
+      (*ctLoader_, &RtViewerApp::HandleCTContentUpdated);
+    
+    Register<DicomVolumeImage::ContentUpdatedMessage>                             
+      (*doseLoader_, &RtViewerApp::HandleDoseLoaded);
+    
+    Register<DicomStructureSetLoader::StructuresReady>                            
+      (*rtstructLoader_, &RtViewerApp::HandleStructuresReady);
+    
+    Register<DicomStructureSetLoader::StructuresUpdated>                          
+      (*rtstructLoader_, &RtViewerApp::HandleStructuresUpdated);
+
+    /**
+    Configure the CT
+    */
+
+
+    std::auto_ptr<GrayscaleStyleConfigurator> style(new GrayscaleStyleConfigurator);
+    style->SetLinearInterpolation(true);
+
+    this->SetCtVolumeSlicer(LAYER_POSITION + 0, ctLoader_, style.release());
+
+    {
+      std::unique_ptr<LookupTableStyleConfigurator> config(new LookupTableStyleConfigurator);
+      config->SetLookupTable(Orthanc::EmbeddedResources::COLORMAP_HOT);
+
+      boost::shared_ptr<DicomVolumeImageMPRSlicer> tmp(new DicomVolumeImageMPRSlicer(doseVolume_));
+      this->SetDoseVolumeSlicer(LAYER_POSITION + 1, tmp, config.release());
+    }
+
+    this->SetStructureSet(LAYER_POSITION + 2, rtstructLoader_);
+
+#if 1 
+    LOG(INFO) << "About to load:";
+    LOG(INFO) << "  CT       : " << ctSeriesId_;;
+    LOG(INFO) << "  RTDOSE   : " << doseInstanceId_;
+    LOG(INFO) << "  RTSTRUCT : " << rtStructInstanceId_;
+    ctLoader_->LoadSeries(ctSeriesId_);
+    doseLoader_->LoadInstance(doseInstanceId_);
+    rtstructLoader_->LoadInstanceFullVisibility(rtStructInstanceId_);
+
+#elif 0
+    /*
+    BGO data
+    http://localhost:8042/twiga-orthanc-viewer-demo/twiga-orthanc-viewer-demo.html?ct-series=a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa
+    &
+    dose-instance=830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb
+    &
+    struct-instance=54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9
+    */
+    ctLoader_->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // CT
+    doseLoader_->LoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb");  // RT-DOSE
+    rtstructLoader_->LoadInstanceFullVisibility("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
+#else
+    //SJO data
+    //ctLoader->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618");  // CT
+    //doseLoader->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6");  // RT-DOSE
+    //rtstructLoader->LoadInstanceFullVisibility("83d9c0c3-913a7fee-610097d7-cbf0522d-fd75bee6");  // RT-STRUCT
+
+    // 2017-05-16
+    ctLoader_->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // CT
+    doseLoader_->LoadInstance("eac822ef-a395f94e-e8121fe0-8411fef8-1f7bffad");  // RT-DOSE
+    rtstructLoader_->LoadInstanceFullVisibility("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
+#endif
   }
 
-  static bool g_stopApplication = false;
-
-
-  void RtViewerApp::Handle(const DicomVolumeImage::GeometryReadyMessage& message)
-  {
-    RetrieveGeometry();
-  }
-
-
+#if 0
   void RtViewerApp::Handle(const OracleCommandExceptionMessage& message)
   {
     const OracleCommandBase& command = dynamic_cast<const OracleCommandBase&>(message.GetOrigin());
@@ -631,8 +706,14 @@
       break;
     }
   }
+#endif
 
 
+  void RtViewerApp::HandleGeometryReady(const DicomVolumeImage::GeometryReadyMessage& message)
+  {
+    RetrieveGeometry();
+  }
+
   void RtViewerApp::HandleCTLoaded(const OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality& message)
   {
     UpdateLayers();
@@ -659,7 +740,7 @@
     UpdateLayers();
   }
 
-  void RtViewerApp::SetCtVolume(int depth,
+  void RtViewerApp::SetCtVolumeSlicer(int depth,
                                             const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
                                             OrthancStone::ILayerStyleConfigurator* style)
   {
@@ -675,7 +756,7 @@
     }
   }
 
-  void RtViewerApp::SetDoseVolume(int depth,
+  void RtViewerApp::SetDoseVolumeSlicer(int depth,
                                             const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
                                             OrthancStone::ILayerStyleConfigurator* style)
   {
@@ -701,170 +782,6 @@
     structLayerSource_.reset(new OrthancStone::VolumeSceneLayerSource(scene, depth, volume));
   }
 
-  void RtViewerApp::Run()
-  {
-    {
-      std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
-      ViewportController& controller = lock->GetController();
-      Scene2D& scene = controller.GetScene();
-      ICompositor& compositor = lock->GetCompositor();
-
-      // False means we do NOT let Windows treat this as a legacy application
-      // that needs to be scaled
-      controller.FitContent(compositor.GetCanvasWidth(), compositor.GetCanvasHeight());
-
-      glEnable(GL_DEBUG_OUTPUT);
-      glDebugMessageCallback(OpenGLMessageCallback, 0);
-
-      compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
-                         FONT_SIZE_0, Orthanc::Encoding_Latin1);
-      compositor.SetFont(1, Orthanc::EmbeddedResources::UBUNTU_FONT,
-                         FONT_SIZE_1, Orthanc::Encoding_Latin1);
-    }
-                     //////// from loader
-    {
-      Orthanc::WebServiceParameters p;
-      //p.SetUrl("http://localhost:8043/");
-      p.SetCredentials("orthanc", "orthanc");
-      oracle_.SetOrthancParameters(p);
-    }
-
-    //////// from Run
-
-    boost::shared_ptr<DicomVolumeImage>  ctVolume(new DicomVolumeImage);
-    boost::shared_ptr<DicomVolumeImage>  doseVolume(new DicomVolumeImage);
-
-
-    boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> ctLoader;
-    boost::shared_ptr<OrthancMultiframeVolumeLoader> doseLoader;
-    boost::shared_ptr<DicomStructureSetLoader>  rtstructLoader;
-
-    {
-      // "true" means use progressive quality (jpeg 50 --> jpeg 90 --> 16-bit raw)
-      ctLoader = OrthancSeriesVolumeProgressiveLoader::Create(*loadersContext_, ctVolume, false);
-      
-      // we need to store the CT loader to ask from geometry details later on when geometry is loaded
-      geometryProvider_ = ctLoader;
-
-      doseLoader = OrthancMultiframeVolumeLoader::Create(*loadersContext_, doseVolume);
-      rtstructLoader = DicomStructureSetLoader::Create(*loadersContext_);
-    }
-
-    Register<DicomVolumeImage::GeometryReadyMessage>(*ctLoader, &RtViewerApp::Handle);
-    Register<OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality>(*ctLoader, &RtViewerApp::HandleCTLoaded);
-    Register<DicomVolumeImage::ContentUpdatedMessage>(*ctLoader, &RtViewerApp::HandleCTContentUpdated);
-    Register<DicomVolumeImage::ContentUpdatedMessage>(*doseLoader, &RtViewerApp::HandleDoseLoaded);
-    Register<DicomStructureSetLoader::StructuresReady>(*rtstructLoader, &RtViewerApp::HandleStructuresReady);
-    Register<DicomStructureSetLoader::StructuresUpdated>(*rtstructLoader, &RtViewerApp::HandleStructuresUpdated);
-
-    std::auto_ptr<GrayscaleStyleConfigurator> style(new GrayscaleStyleConfigurator);
-    style->SetLinearInterpolation(true);
-
-    this->SetCtVolume(LAYER_POSITION + 0, ctLoader, style.release());
-
-    {
-      std::unique_ptr<LookupTableStyleConfigurator> config(new LookupTableStyleConfigurator);
-      config->SetLookupTable(Orthanc::EmbeddedResources::COLORMAP_HOT);
-
-      boost::shared_ptr<DicomVolumeImageMPRSlicer> tmp(new DicomVolumeImageMPRSlicer(doseVolume));
-      this->SetDoseVolume(LAYER_POSITION + 1, tmp, config.release());
-    }
-
-    this->SetStructureSet(LAYER_POSITION + 2, rtstructLoader);
-
-#if 1
-    /*
-    BGO data
-    http://localhost:8042/twiga-orthanc-viewer-demo/twiga-orthanc-viewer-demo.html?ct-series=a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa
-    &
-    dose-instance=830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb
-    &
-    struct-instance=54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9
-    */
-    ctLoader->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // CT
-    doseLoader->LoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb");  // RT-DOSE
-    rtstructLoader->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
-#else
-    //ctLoader->LoadSeries("cb3ea4d1-d08f3856-ad7b6314-74d88d77-60b05618");  // CT
-    //doseLoader->LoadInstance("41029085-71718346-811efac4-420e2c15-d39f99b6");  // RT-DOSE
-    //rtstructLoader->LoadInstance("83d9c0c3-913a7fee-610097d7-cbf0522d-fd75bee6");  // RT-STRUCT
-
-    // 2017-05-16
-    ctLoader->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // CT
-    doseLoader->LoadInstance("eac822ef-a395f94e-e8121fe0-8411fef8-1f7bffad");  // RT-DOSE
-    rtstructLoader->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
-#endif
-
-    oracle_.Start();
-
-//// END from loader
-
-    while (!g_stopApplication)
-    {
-      //compositor.Refresh(scene);
-
-//////// from loader
-//// END from loader
-
-      SDL_Event event;
-      while (!g_stopApplication && SDL_PollEvent(&event))
-      {
-        if (event.type == SDL_QUIT)
-        {
-          g_stopApplication = true;
-          break;
-        }
-        else if (event.type == SDL_WINDOWEVENT &&
-                 event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
-        {
-          DisableTracker(); // was: tracker.reset(NULL);
-        }
-        else if (event.type == SDL_KEYDOWN &&
-                 event.key.repeat == 0 /* Ignore key bounce */)
-        {
-          switch (event.key.keysym.sym)
-          {
-          case SDLK_f:
-            // TODO: implement GetWindow!!!
-            // viewport_->GetContext()->GetWindow().ToggleMaximize();
-            ORTHANC_ASSERT(false, "Please implement GetWindow()");
-            break;
-
-          case SDLK_r:
-            break;
-
-          case SDLK_s:
-            FitContent();
-            break;
-
-          case SDLK_q:
-            g_stopApplication = true;
-            break;
-          default:
-            break;
-          }
-        }
-        HandleApplicationEvent(event);
-      }
-      SDL_Delay(1);
-    }
-
-    //// from loader
-
-    //Orthanc::SystemToolbox::ServerBarrier();
-
-    /**
-     * WARNING => The oracle must be stopped BEFORE the objects using
-     * it are destroyed!!! This forces to wait for the completion of
-     * the running callback methods. Otherwise, the callbacks methods
-     * might still be running while their parent object is destroyed,
-     * resulting in crashes. This is very visible if adding a sleep(),
-     * as in (*).
-     **/
-
-    oracle_.Stop();
-    //// END from loader
-  }
 
   void RtViewerApp::SetInfoDisplayMessage(
     std::string key, std::string value)
@@ -900,15 +817,12 @@
   using namespace OrthancStone;
 
   StoneInitialize();
-  Orthanc::Logging::EnableInfoLevel(true);
-  //Orthanc::Logging::EnableTraceLevel(true);
 
   try
   {
     boost::shared_ptr<RtViewerApp> app = RtViewerApp::Create();
     g_app = app;
-    //app->PrepareScene();
-    app->Run();
+    app->RunSdl(argc,argv);
   }
   catch (Orthanc::OrthancException& e)
   {
--- a/Samples/Common/RtViewer.h	Mon Apr 27 16:47:46 2020 +0200
+++ b/Samples/Common/RtViewer.h	Mon Apr 27 16:48:19 2020 +0200
@@ -36,7 +36,9 @@
 #include <boost/thread.hpp>
 #include <boost/noncopyable.hpp>
 
+#if ORTHANC_ENABLE_SDL
 #include <SDL.h>
+#endif
 
 namespace OrthancStone
 {
@@ -74,18 +76,24 @@
   can be sent from multiple threads)
   */
   class RtViewerApp : public ObserverBase<RtViewerApp>
-    , public IMessageEmitter
   {
   public:
 
+    void PrepareScene();
 
-    void PrepareScene();
-    void Run();
+#if ORTHANC_ENABLE_SDL
+  public:
+    void RunSdl(int argc, char* argv[]);
+  private:
+    void ProcessOptions(int argc, char* argv[]);
+    void HandleApplicationEvent(const SDL_Event& event);
+#elif ORTHANC_ENABLE_WASM
+#endif
+
+  public:
     void SetInfoDisplayMessage(std::string key, std::string value);
     void DisableTracker();
 
-    void HandleApplicationEvent(const SDL_Event& event);
-
     /**
     This method is called when the scene transform changes. It allows to
     recompute the visual elements whose content depend upon the scene transform
@@ -103,6 +111,7 @@
 
     void Refresh();
 
+#if 0
     virtual void EmitMessage(boost::weak_ptr<IObserver> observer,
       const IMessage& message) ORTHANC_OVERRIDE
     {
@@ -117,6 +126,7 @@
         throw;
       }
     }
+#endif
 
     static boost::shared_ptr<RtViewerApp> Create();
     void RegisterMessages();
@@ -125,11 +135,42 @@
     RtViewerApp();
 
   private:
-#if 1
+    void PrepareLoadersAndSlicers();
+
+    /**
+    Url of the Orthanc instance
+    Typically, in a native application (Qt, SDL), it will be an absolute URL like "http://localhost:8042". In 
+    wasm on the browser, it could be an absolute URL, provided you do not have cross-origin problems, or a relative
+    URL. In our wasm samples, it is set to "..", because we set up either a reverse proxy or an Orthanc ServeFolders
+    plugin that serves the main web application from an URL like "http://localhost:8042/rtviewer" (with ".." leading 
+    to the main Orthanc root URL)
+    */
+    std::string orthancUrl_;
+
+    /**
+    Orthanc ID of the CT series to load. Only used between startup and loading time.
+    */
+    std::string ctSeriesId_;
+
+    /**
+    Orthanc ID of the RTDOSE instance to load. Only used between startup and loading time.
+    */
+    std::string doseInstanceId_;
+
+    /**
+    Orthanc ID of the RTSTRUCT instance to load. Only used between startup and loading time.
+    */
+    std::string rtStructInstanceId_;
+
+
+#if ORTHANC_ENABLE_SDL
     // if threaded (not wasm)
-    IObservable oracleObservable_;
-    ThreadedOracle oracle_;
-    boost::shared_mutex mutex_; // to serialize messages from the ThreadedOracle
+    //IObservable oracleObservable_;
+    //ThreadedOracle oracle_;
+    //boost::shared_mutex mutex_; // to serialize messages from the ThreadedOracle
+#elif ORTHANC_ENABLE_WASM
+
+
 #endif
 
     void SelectNextTool();
@@ -159,8 +200,7 @@
     void Redo();
 
 
-    void Handle(const DicomVolumeImage::GeometryReadyMessage& message);
-    void Handle(const OracleCommandExceptionMessage& message);
+    void HandleGeometryReady(const DicomVolumeImage::GeometryReadyMessage& message);
     
     // TODO: wire this
     void HandleCTLoaded(const OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality& message);
@@ -169,12 +209,12 @@
     void HandleStructuresReady(const OrthancStone::DicomStructureSetLoader::StructuresReady& message);
     void HandleStructuresUpdated(const OrthancStone::DicomStructureSetLoader::StructuresUpdated& message);
 
-    void SetCtVolume(
+    void SetCtVolumeSlicer(
       int depth,
       const boost::shared_ptr<IVolumeSlicer>& volume,
       ILayerStyleConfigurator* style);
     
-    void SetDoseVolume(
+    void SetDoseVolumeSlicer(
       int depth,
       const boost::shared_ptr<IVolumeSlicer>& volume,
       ILayerStyleConfigurator* style);
@@ -191,9 +231,25 @@
     void FitContent();
 
   private:
-    boost::shared_ptr<GenericLoadersContext> loadersContext_;
-    boost::shared_ptr<VolumeSceneLayerSource>  ctVolumeLayerSource_, doseVolumeLayerSource_, structLayerSource_;
-    boost::shared_ptr<OrthancStone::IGeometryProvider> geometryProvider_;
+    boost::shared_ptr<DicomVolumeImage>  ctVolume_;
+    boost::shared_ptr<DicomVolumeImage>  doseVolume_;
+
+    boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> ctLoader_;
+    boost::shared_ptr<OrthancMultiframeVolumeLoader> doseLoader_;
+    boost::shared_ptr<DicomStructureSetLoader>  rtstructLoader_;
+
+    /** encapsulates resources shared by loaders */
+    boost::shared_ptr<GenericLoadersContext>            loadersContext_;
+    boost::shared_ptr<VolumeSceneLayerSource>           ctVolumeLayerSource_, doseVolumeLayerSource_, structLayerSource_;
+    
+    /**
+    another interface to the ctLoader object (that also implements the IVolumeSlicer interface), that serves as the 
+    reference for the geometry (position and dimensions of the volume + size of each voxel). It could be changed to be 
+    the dose instead, but the CT is chosen because it usually has a better spatial resolution.
+    */
+    boost::shared_ptr<OrthancStone::IGeometryProvider>  geometryProvider_;
+
+    // collection of cutting planes for this particular view
     std::vector<OrthancStone::CoordinateSystem3D>       planes_;
     size_t                                              currentPlane_;
 
--- a/Samples/Common/SampleHelpers.h	Mon Apr 27 16:47:46 2020 +0200
+++ b/Samples/Common/SampleHelpers.h	Mon Apr 27 16:48:19 2020 +0200
@@ -1,10 +1,12 @@
 #pragma once
 
+#include <Core/Logging.h>
+
+#include <boost/algorithm/string.hpp>
+
 #include <string>
 #include <iostream>
 
-#include <Core/Logging.h>
-
 namespace OrthancStoneHelpers
 {
   inline void SetLogLevel(std::string logLevel)
@@ -32,4 +34,4 @@
       Orthanc::Logging::EnableTraceLevel(true);
     }
   }
-}
\ No newline at end of file
+}
--- a/Samples/Sdl/RtViewer/CMakeLists.txt	Mon Apr 27 16:47:46 2020 +0200
+++ b/Samples/Sdl/RtViewer/CMakeLists.txt	Mon Apr 27 16:48:19 2020 +0200
@@ -31,6 +31,7 @@
 include(${STONE_ROOT}/Resources/CMake/Utilities.cmake)
 
 include_directories(${STONE_ROOT})
+include_directories(../../Common)
 
 add_definitions(
   -DORTHANC_ENABLE_LOGGING=1
@@ -42,13 +43,13 @@
 SortFilesInSourceGroups()
 
 add_executable(RtViewerSdl
+  RtViewerSdl.cpp
   ../SdlHelpers.h
-  RtViewerSdl.cpp
   ../../Common/RtViewer.cpp
   ../../Common/RtViewer.h
+  ../../Common/SampleHelpers.h
   ${ORTHANC_STONE_SOURCES}
   )
 
-
 target_link_libraries(RtViewerSdl ${DCMTK_LIBRARIES})
 
--- a/Samples/Sdl/RtViewer/RtViewerSdl.cpp	Mon Apr 27 16:47:46 2020 +0200
+++ b/Samples/Sdl/RtViewer/RtViewerSdl.cpp	Mon Apr 27 16:48:19 2020 +0200
@@ -0,0 +1,191 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#include "RtViewer.h"
+#include "SampleHelpers.h"
+
+#include <Framework/StoneException.h>
+
+#include <boost/program_options.hpp>
+#include <SDL.h>
+
+#include <string>
+
+static void GLAPIENTRY
+OpenGLMessageCallback(GLenum source,
+                      GLenum type,
+                      GLuint id,
+                      GLenum severity,
+                      GLsizei length,
+                      const GLchar* message,
+                      const void* userParam)
+{
+  if (severity != GL_DEBUG_SEVERITY_NOTIFICATION)
+  {
+    fprintf(stderr, "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
+            (type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""),
+            type, severity, message);
+  }
+}
+
+namespace OrthancStone
+{
+  void RtViewerApp::ProcessOptions(int argc, char* argv[])
+  {
+    namespace po = boost::program_options;
+    po::options_description desc("Usage:");
+
+    //ctLoader->LoadSeries("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa");  // CT
+    //doseLoader->LoadInstance("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb");  // RT-DOSE
+    //rtstructLoader->LoadInstance("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9");  // RT-STRUCT
+
+
+    desc.add_options()
+      ("loglevel", po::value<std::string>()->default_value("WARNING"),
+       "You can choose WARNING, INFO or TRACE for the logging level: Errors and warnings will always be displayed. (default: WARNING)")
+
+      ("orthanc", po::value<std::string>()->default_value("http://localhost:8042"),
+       "Base URL of the Orthanc instance")
+
+      ("ctseries", po::value<std::string>()->default_value("a04ecf01-79b2fc33-58239f7e-ad9db983-28e81afa"),
+       "Orthanc ID of the CT series to load")
+
+      ("rtdose", po::value<std::string>()->default_value("830a69ff-8e4b5ee3-b7f966c8-bccc20fb-d322dceb"),
+       "Orthanc ID of the RTDOSE instance to load")
+
+      ("rtstruct", po::value<std::string>()->default_value("54460695-ba3885ee-ddf61ac0-f028e31d-a6e474d9"),
+       "Orthanc ID of the RTSTRUCT instance to load")
+      ;
+
+    po::variables_map vm;
+    try
+    {
+      po::store(po::parse_command_line(argc, argv, desc), vm);
+      po::notify(vm);
+    }
+    catch (std::exception& e)
+    {
+      std::cerr << "Please check your command line options! (\"" << e.what() << "\")" << std::endl;
+    }
+
+    if (vm.count("loglevel") > 0)
+    {
+      std::string logLevel = vm["loglevel"].as<std::string>();
+      OrthancStoneHelpers::SetLogLevel(logLevel);
+    }
+
+    if (vm.count("orthanc") > 0)
+    {
+      // maybe check URL validity here
+      orthancUrl_ = vm["orthanc"].as<std::string>();
+    }
+
+    if (vm.count("ctseries") > 0)
+    {
+      ctSeriesId_ = vm["ctseries"].as<std::string>();
+    }
+
+    if (vm.count("rtdose") > 0)
+    {
+      doseInstanceId_ = vm["rtdose"].as<std::string>();
+    }
+
+    if (vm.count("rtstruct") > 0)
+    {
+      rtStructInstanceId_ = vm["rtstruct"].as<std::string>();
+    }
+  }
+
+  void RtViewerApp::RunSdl(int argc, char* argv[])
+  {
+    ProcessOptions(argc, argv);
+
+    {
+      std::unique_ptr<IViewport::ILock> lock(viewport_->Lock());
+      ViewportController& controller = lock->GetController();
+      Scene2D& scene = controller.GetScene();
+      ICompositor& compositor = lock->GetCompositor();
+
+      // False means we do NOT let Windows treat this as a legacy application
+      // that needs to be scaled
+      controller.FitContent(compositor.GetCanvasWidth(), compositor.GetCanvasHeight());
+
+      glEnable(GL_DEBUG_OUTPUT);
+      glDebugMessageCallback(OpenGLMessageCallback, 0);
+
+      compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
+                         FONT_SIZE_0, Orthanc::Encoding_Latin1);
+      compositor.SetFont(1, Orthanc::EmbeddedResources::UBUNTU_FONT,
+                         FONT_SIZE_1, Orthanc::Encoding_Latin1);
+    }
+                     //////// from loader
+    
+    loadersContext_.reset(new GenericLoadersContext(1, 4, 1));
+    loadersContext_->StartOracle();
+
+    /**
+    It is very important that the Oracle (responsible for network I/O be started before creating and firing the 
+    loaders, for any command scheduled by the loader before the oracle is started will be lost.
+    */
+    PrepareLoadersAndSlicers();
+
+    bool stopApplication = false;
+
+    while (!stopApplication)
+    {
+      //compositor.Refresh(scene);
+
+      SDL_Event event;
+      while (!stopApplication && SDL_PollEvent(&event))
+      {
+        if (event.type == SDL_QUIT)
+        {
+          stopApplication = true;
+          break;
+        }
+        else if (event.type == SDL_WINDOWEVENT &&
+                 event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
+        {
+          DisableTracker(); // was: tracker.reset(NULL);
+        }
+        else if (event.type == SDL_KEYDOWN &&
+                 event.key.repeat == 0 /* Ignore key bounce */)
+        {
+          switch (event.key.keysym.sym)
+          {
+          case SDLK_f:
+            // TODO: implement GetWindow!!!
+            // viewport_->GetContext()->GetWindow().ToggleMaximize();
+            ORTHANC_ASSERT(false, "Please implement GetWindow()");
+            break;
+          case SDLK_q:
+            stopApplication = true;
+            break;
+          default:
+            break;
+          }
+        }
+        HandleApplicationEvent(event);
+      }
+      SDL_Delay(1);
+    }
+    loadersContext_->StopOracle();
+  }
+}
--- a/Samples/Sdl/SdlHelpers.h	Mon Apr 27 16:47:46 2020 +0200
+++ b/Samples/Sdl/SdlHelpers.h	Mon Apr 27 16:48:19 2020 +0200
@@ -1,5 +1,11 @@
 #pragma once
 
+#if ORTHANC_ENABLE_SDL != 1
+# error This file cannot be used if ORTHANC_ENABLE_SDL != 1
+#endif
+
+#include <SDL.h>
+
 #include <map>
 #include <string>