changeset 941:acc172f2b782

merge
author Alain Mazy <alain@mazy.be>
date Fri, 02 Aug 2019 17:38:44 +0200
parents 861c080ef47b (current diff) ab90628e70d9 (diff)
children 8b2d0d2c17a6
files
diffstat 24 files changed, 598 insertions(+), 103 deletions(-) [+]
line wrap: on
line diff
--- a/.hgtags	Fri Aug 02 17:38:31 2019 +0200
+++ b/.hgtags	Fri Aug 02 17:38:44 2019 +0200
@@ -5,3 +5,7 @@
 fe96057e97b94eb8a46c1a33ba350c354b5c4afc toa2019062502
 60a403f01c3112249f9d4a1a6149bef1de9766bf toa2019062503
 81d30cd93b6586c2d190283eb23622822db0666d toa2019072201
+e594e76e0a81d6342e7e18b67e9ac2fe62a6ce62 toa2019072202
+fc38c4ab17e31b3ccc20c8d8bc557ae8ad4c3b0e toa2019072401
+401808e7ff2e0f8cb889f9fa90baaddad1c97e57 toa2019072901
+eaaa9b574e054feed6f0e71302b14b068cfe338a toa2019073101
--- a/Applications/Generic/GuiAdapter.cpp	Fri Aug 02 17:38:31 2019 +0200
+++ b/Applications/Generic/GuiAdapter.cpp	Fri Aug 02 17:38:44 2019 +0200
@@ -51,7 +51,7 @@
   }
 
 #if ORTHANC_ENABLE_WASM == 1
-  void GuiAdapter::Run()
+  void GuiAdapter::Run(GuiAdapterRunFunc /*func*/, void* /*cookie*/)
   {
   }
 
@@ -723,7 +723,7 @@
 # endif
 
   // SDL ONLY
-  void GuiAdapter::Run()
+  void GuiAdapter::Run(GuiAdapterRunFunc func, void* cookie)
   {
 #if 1
     // TODO: MAKE THIS DYNAMIC !!! See SdlOpenGLViewport vs Cairo in ViewportWrapper
@@ -741,6 +741,8 @@
     {
       {
         LockingEmitter::WriterLock lock(lockingEmitter_);
+        if(func != NULL)
+          (*func)(cookie);
         OnAnimationFrame(); // in SDL we must call it
       }
 
--- a/Applications/Generic/GuiAdapter.h	Fri Aug 02 17:38:31 2019 +0200
+++ b/Applications/Generic/GuiAdapter.h	Fri Aug 02 17:38:44 2019 +0200
@@ -218,6 +218,8 @@
 
 #endif
 
+  typedef void (*GuiAdapterRunFunc)(void*);
+
   class GuiAdapter
   {
   public:
@@ -276,7 +278,7 @@
     Under wasm, it returns without doing anything, since the event loop is managed
     by the browser.
     */
-    void Run();
+    void Run(GuiAdapterRunFunc func = NULL, void* cookie = NULL);
 
 #if ORTHANC_ENABLE_WASM != 1
     /**
@@ -364,7 +366,12 @@
       for (size_t i = 0; i < widgets_.size(); i++)
       {
         boost::shared_ptr<IGuiAdapterWidget> widget = widgets_[i].lock();
-        func(widget);
+
+        // TODO: we need to clean widgets!
+        if (widget.get() != NULL)
+        {
+          func(widget);
+        }
       }
     }
   };
--- a/Framework/Loaders/DicomStructureSetLoader.cpp	Fri Aug 02 17:38:31 2019 +0200
+++ b/Framework/Loaders/DicomStructureSetLoader.cpp	Fri Aug 02 17:38:44 2019 +0200
@@ -58,6 +58,7 @@
         // All the referenced instances have been loaded, finalize the RT-STRUCT
         loader.content_->CheckReferencedSlices();
         loader.revision_++;
+        loader.SetStructuresReady();
       }
     }
   };
@@ -225,14 +226,21 @@
 
   DicomStructureSetLoader::DicomStructureSetLoader(IOracle& oracle,
                                                    IObservable& oracleObservable) :
+    IObservable(oracleObservable.GetBroker()),
     LoaderStateMachine(oracle, oracleObservable),
     revision_(0),
     countProcessedInstances_(0),
-    countReferencedInstances_(0)
+    countReferencedInstances_(0),
+    structuresReady_(false)
   {
   }
     
     
+  DicomStructureSetLoader::~DicomStructureSetLoader()
+  {
+    LOG(TRACE) << "DicomStructureSetLoader::~DicomStructureSetLoader()";
+  }
+
   void DicomStructureSetLoader::LoadInstance(const std::string& instanceId)
   {
     Start();
@@ -261,4 +269,17 @@
       return new Slice(*content_, revision_, cuttingPlane);
     }
   }
+
+  void DicomStructureSetLoader::SetStructuresReady()
+  {
+    ORTHANC_ASSERT(!structuresReady_);
+    structuresReady_ = true;
+    BroadcastMessage(DicomStructureSetLoader::StructuresReady(*this));
+  }
+
+  bool DicomStructureSetLoader::AreStructuresReady() const
+  {
+    return structuresReady_;
+  }
+
 }
--- a/Framework/Loaders/DicomStructureSetLoader.h	Fri Aug 02 17:38:31 2019 +0200
+++ b/Framework/Loaders/DicomStructureSetLoader.h	Fri Aug 02 17:38:44 2019 +0200
@@ -29,7 +29,8 @@
 {
   class DicomStructureSetLoader :
     public LoaderStateMachine,
-    public IVolumeSlicer
+    public IVolumeSlicer,
+    public IObservable
   {
   private:
     class Slice;
@@ -44,13 +45,24 @@
     std::string                       instanceId_;
     unsigned int                      countProcessedInstances_;
     unsigned int                      countReferencedInstances_;  
+
+    // will be set to true once the loading is finished
+    bool                              structuresReady_;
     
   public:
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, StructuresReady, DicomStructureSetLoader);
+
     DicomStructureSetLoader(IOracle& oracle,
                             IObservable& oracleObservable);    
     
+    ~DicomStructureSetLoader();
+    
     void LoadInstance(const std::string& instanceId);
 
     virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane);
+
+    void SetStructuresReady();
+
+    bool AreStructuresReady() const;
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/LoaderCache.cpp	Fri Aug 02 17:38:44 2019 +0200
@@ -0,0 +1,249 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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 "LoaderCache.h"
+
+#include "OrthancSeriesVolumeProgressiveLoader.h"
+#include "OrthancMultiframeVolumeLoader.h"
+#include "DicomStructureSetLoader.h"
+
+#if ORTHANC_ENABLE_WASM == 1
+# include <unistd.h>
+# include "../Oracle/WebAssemblyOracle.h"
+#else
+# include "../Oracle/ThreadedOracle.h"
+#endif
+
+#include "../Messages/LockingEmitter.h"
+#include "../Volumes/DicomVolumeImage.h"
+#include "../Volumes/DicomVolumeImageMPRSlicer.h"
+
+#include <Core/OrthancException.h>
+#include <Core/Toolbox.h>
+
+namespace OrthancStone
+{
+#if ORTHANC_ENABLE_WASM == 1
+  LoaderCache::LoaderCache(WebAssemblyOracle& oracle)
+    : oracle_(oracle)
+  {
+
+  }
+#else
+  LoaderCache::LoaderCache(ThreadedOracle& oracle, LockingEmitter& lockingEmitter)
+    : oracle_(oracle)
+    , lockingEmitter_(lockingEmitter)
+  {
+  }
+#endif
+
+  boost::shared_ptr<OrthancStone::OrthancSeriesVolumeProgressiveLoader> 
+    LoaderCache::GetSeriesVolumeProgressiveLoader(std::string seriesUuid)
+  {
+    try
+    {
+      
+      // normalize keys a little
+      seriesUuid = Orthanc::Toolbox::StripSpaces(seriesUuid);
+      Orthanc::Toolbox::ToLowerCase(seriesUuid);
+
+      // find in cache
+      if (seriesVolumeProgressiveLoaders_.find(seriesUuid) == seriesVolumeProgressiveLoaders_.end())
+      {
+//        LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : CACHEMISS --> need to load seriesUUid = " << seriesUuid;
+#if ORTHANC_ENABLE_WASM == 1
+//        LOG(TRACE) << "Performing request for series " << seriesUuid << " sbrk(0) = " << sbrk(0);
+#else
+//        LOG(TRACE) << "Performing request for series " << seriesUuid;
+#endif
+        boost::shared_ptr<DicomVolumeImage> volumeImage(new DicomVolumeImage);
+        boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> loader;
+//        LOG(TRACE) << "volumeImage = " << volumeImage.get();
+        {
+#if ORTHANC_ENABLE_WASM == 1
+          loader.reset(new OrthancSeriesVolumeProgressiveLoader(volumeImage, oracle_, oracle_));
+#else
+          LockingEmitter::WriterLock lock(lockingEmitter_);
+          loader.reset(new OrthancSeriesVolumeProgressiveLoader(volumeImage, oracle_, lock.GetOracleObservable()));
+#endif
+//          LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : loader = " << loader.get();
+          loader->LoadSeries(seriesUuid);
+//          LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : loader->LoadSeries successful";
+        }
+        seriesVolumeProgressiveLoaders_[seriesUuid] = loader;
+      }
+      else
+      {
+//        LOG(TRACE) << "LoaderCache::GetSeriesVolumeProgressiveLoader : returning cached loader for seriesUUid = " << seriesUuid;
+      }
+      return seriesVolumeProgressiveLoaders_[seriesUuid];
+    }
+    catch (const Orthanc::OrthancException& e)
+    {
+      if (e.HasDetails())
+      {
+        LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails();
+      }
+      else
+      {
+        LOG(ERROR) << "OrthancException in LoaderCache: " << e.What();
+      }
+      throw;
+    }
+    catch (const std::exception& e)
+    {
+      LOG(ERROR) << "std::exception in LoaderCache: " << e.what();
+      throw;
+    }
+    catch (...)
+    {
+      LOG(ERROR) << "Unknown exception in LoaderCache";
+      throw;
+    }
+  }
+
+  boost::shared_ptr<OrthancMultiframeVolumeLoader> LoaderCache::GetMultiframeVolumeLoader(std::string instanceUuid)
+  {
+    // if the loader is not available, let's trigger its creation
+    if(multiframeVolumeLoaders_.find(instanceUuid) == multiframeVolumeLoaders_.end())
+    {
+      GetMultiframeDicomVolumeImageMPRSlicer(instanceUuid);
+    }
+    ORTHANC_ASSERT(multiframeVolumeLoaders_.find(instanceUuid) != multiframeVolumeLoaders_.end());
+
+    return multiframeVolumeLoaders_[instanceUuid];
+  }
+
+  boost::shared_ptr<DicomVolumeImageMPRSlicer> LoaderCache::GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid)
+  {
+    try
+    {
+      // normalize keys a little
+      instanceUuid = Orthanc::Toolbox::StripSpaces(instanceUuid);
+      Orthanc::Toolbox::ToLowerCase(instanceUuid);
+
+      // find in cache
+      if (dicomVolumeImageMPRSlicers_.find(instanceUuid) == dicomVolumeImageMPRSlicers_.end())
+      {
+        boost::shared_ptr<DicomVolumeImage> volumeImage(new DicomVolumeImage);
+        boost::shared_ptr<OrthancMultiframeVolumeLoader> loader;
+
+        {
+#if ORTHANC_ENABLE_WASM == 1
+          loader.reset(new OrthancMultiframeVolumeLoader(volumeImage, oracle_, oracle_));
+#else
+          LockingEmitter::WriterLock lock(lockingEmitter_);
+          loader.reset(new OrthancMultiframeVolumeLoader(volumeImage, oracle_, lock.GetOracleObservable()));
+#endif
+          loader->LoadInstance(instanceUuid);
+        }
+        multiframeVolumeLoaders_[instanceUuid] = loader;
+        boost::shared_ptr<DicomVolumeImageMPRSlicer> mprSlicer(new DicomVolumeImageMPRSlicer(volumeImage));
+        dicomVolumeImageMPRSlicers_[instanceUuid] = mprSlicer;
+      }
+      return dicomVolumeImageMPRSlicers_[instanceUuid];
+    }
+    catch (const Orthanc::OrthancException& e)
+    {
+      if (e.HasDetails())
+      {
+        LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails();
+      }
+      else
+      {
+        LOG(ERROR) << "OrthancException in LoaderCache: " << e.What();
+      }
+      throw;
+    }
+    catch (const std::exception& e)
+    {
+      LOG(ERROR) << "std::exception in LoaderCache: " << e.what();
+      throw;
+    }
+    catch (...)
+    {
+      LOG(ERROR) << "Unknown exception in LoaderCache";
+      throw;
+    }
+  }
+  
+  boost::shared_ptr<DicomStructureSetLoader> LoaderCache::GetDicomStructureSetLoader(std::string instanceUuid)
+  {
+    try
+    {
+      // normalize keys a little
+      instanceUuid = Orthanc::Toolbox::StripSpaces(instanceUuid);
+      Orthanc::Toolbox::ToLowerCase(instanceUuid);
+
+      // find in cache
+      if (dicomStructureSetLoaders_.find(instanceUuid) == dicomStructureSetLoaders_.end())
+      {
+        boost::shared_ptr<DicomStructureSetLoader> loader;
+
+        {
+#if ORTHANC_ENABLE_WASM == 1
+          loader.reset(new DicomStructureSetLoader(oracle_, oracle_));
+#else
+          LockingEmitter::WriterLock lock(lockingEmitter_);
+          loader.reset(new DicomStructureSetLoader(oracle_, lock.GetOracleObservable()));
+#endif
+          loader->LoadInstance(instanceUuid);
+        }
+        dicomStructureSetLoaders_[instanceUuid] = loader;
+      }
+      return dicomStructureSetLoaders_[instanceUuid];
+    }
+    catch (const Orthanc::OrthancException& e)
+    {
+      if (e.HasDetails())
+      {
+        LOG(ERROR) << "OrthancException in LoaderCache: " << e.What() << " Details: " << e.GetDetails();
+      }
+      else
+      {
+        LOG(ERROR) << "OrthancException in LoaderCache: " << e.What();
+      }
+      throw;
+    }
+    catch (const std::exception& e)
+    {
+      LOG(ERROR) << "std::exception in LoaderCache: " << e.what();
+      throw;
+    }
+    catch (...)
+    {
+      LOG(ERROR) << "Unknown exception in LoaderCache";
+      throw;
+    }
+  }
+
+
+  void LoaderCache::ClearCache()
+  {
+#if ORTHANC_ENABLE_WASM != 1
+    LockingEmitter::WriterLock lock(lockingEmitter_);
+#endif
+    seriesVolumeProgressiveLoaders_.clear();
+    multiframeVolumeLoaders_.clear();
+    dicomVolumeImageMPRSlicers_.clear();
+    dicomStructureSetLoaders_.clear();
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Loaders/LoaderCache.h	Fri Aug 02 17:38:44 2019 +0200
@@ -0,0 +1,84 @@
+/**
+ * Stone of Orthanc
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2019 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/>.
+ **/
+
+#pragma once
+
+#include <boost/shared_ptr.hpp>
+
+#include <map>
+#include <string>
+
+namespace OrthancStone
+{
+  class OrthancSeriesVolumeProgressiveLoader;
+  class DicomVolumeImageMPRSlicer;
+  class DicomStructureSetLoader;
+  class OrthancMultiframeVolumeLoader;
+
+#if ORTHANC_ENABLE_WASM == 1
+  class WebAssemblyOracle;
+#else
+  class ThreadedOracle;
+  class LockingEmitter;
+#endif
+
+  class LoaderCache
+  {
+  public:
+#if ORTHANC_ENABLE_WASM == 1
+    LoaderCache(WebAssemblyOracle& oracle);
+#else
+    LoaderCache(ThreadedOracle& oracle, LockingEmitter& lockingEmitter);
+#endif
+
+    boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader>
+      GetSeriesVolumeProgressiveLoader      (std::string seriesUuid);
+    
+    boost::shared_ptr<DicomVolumeImageMPRSlicer>
+      GetMultiframeDicomVolumeImageMPRSlicer(std::string instanceUuid);
+
+    boost::shared_ptr<OrthancMultiframeVolumeLoader>
+      GetMultiframeVolumeLoader(std::string instanceUuid);
+
+    boost::shared_ptr<DicomStructureSetLoader>
+      GetDicomStructureSetLoader            (std::string instanceUuid);
+
+    void ClearCache();
+
+  private:
+    
+#if ORTHANC_ENABLE_WASM == 1
+    WebAssemblyOracle& oracle_;
+#else
+    ThreadedOracle& oracle_;
+    LockingEmitter& lockingEmitter_;
+#endif
+
+    std::map<std::string, boost::shared_ptr<OrthancSeriesVolumeProgressiveLoader> >
+      seriesVolumeProgressiveLoaders_;
+    std::map<std::string, boost::shared_ptr<OrthancMultiframeVolumeLoader> >
+      multiframeVolumeLoaders_;
+    std::map<std::string, boost::shared_ptr<DicomVolumeImageMPRSlicer> >
+      dicomVolumeImageMPRSlicers_;
+    std::map<std::string, boost::shared_ptr<DicomStructureSetLoader> >
+      dicomStructureSetLoaders_;
+  };
+}
+
--- a/Framework/Loaders/OrthancMultiframeVolumeLoader.cpp	Fri Aug 02 17:38:31 2019 +0200
+++ b/Framework/Loaders/OrthancMultiframeVolumeLoader.cpp	Fri Aug 02 17:38:44 2019 +0200
@@ -127,8 +127,7 @@
       GetLoader<OrthancMultiframeVolumeLoader>().SetTransferSyntax(message.GetAnswer());
     }
   };
-   
-    
+
   class OrthancMultiframeVolumeLoader::LoadUncompressedPixelData : public State
   {
   public:
@@ -142,8 +141,7 @@
       GetLoader<OrthancMultiframeVolumeLoader>().SetUncompressedPixelData(message.GetAnswer());
     }
   };
-   
-    
+
   const std::string& OrthancMultiframeVolumeLoader::GetInstanceId() const
   {
     if (IsActive())
@@ -156,7 +154,6 @@
     }
   }
 
-
   void OrthancMultiframeVolumeLoader::ScheduleFrameDownloads()
   {
     if (transferSyntaxUid_.empty() ||
@@ -243,13 +240,18 @@
 
 
   ORTHANC_FORCE_INLINE
-  static void CopyPixel(uint32_t& target,
-                        const void* source)
+  static void CopyPixel(uint32_t& target, const void* source)
   {
     // TODO - check alignement?
     target = le32toh(*reinterpret_cast<const uint32_t*>(source));
   }
-      
+
+  ORTHANC_FORCE_INLINE
+    static void CopyPixel(uint16_t& target, const void* source)
+  {
+    // TODO - check alignement?
+    target = le16toh(*reinterpret_cast<const uint16_t*>(source));
+  }
 
   template <typename T>
   void OrthancMultiframeVolumeLoader::CopyPixelData(const std::string& pixelData)
@@ -305,13 +307,16 @@
       case Orthanc::PixelFormat_Grayscale32:
         CopyPixelData<uint32_t>(pixelData);
         break;
-
+      case Orthanc::PixelFormat_Grayscale16:
+        CopyPixelData<uint16_t>(pixelData);
+        break;
       default:
         throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
     }
 
     volume_->IncrementRevision();
 
+    pixelDataLoaded_ = true;
     BroadcastMessage(DicomVolumeImage::ContentUpdatedMessage(*volume_));
   }
 
@@ -321,7 +326,8 @@
                                                                IObservable& oracleObservable) :
     LoaderStateMachine(oracle, oracleObservable),
     IObservable(oracleObservable.GetBroker()),
-    volume_(volume)
+    volume_(volume),
+    pixelDataLoaded_(false)
   {
     if (volume.get() == NULL)
     {
@@ -329,6 +335,10 @@
     }
   }
 
+  OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader()
+  {
+    LOG(TRACE) << "OrthancMultiframeVolumeLoader::~OrthancMultiframeVolumeLoader()";
+  }
 
   void OrthancMultiframeVolumeLoader::LoadInstance(const std::string& instanceId)
   {
--- a/Framework/Loaders/OrthancMultiframeVolumeLoader.h	Fri Aug 02 17:38:31 2019 +0200
+++ b/Framework/Loaders/OrthancMultiframeVolumeLoader.h	Fri Aug 02 17:38:44 2019 +0200
@@ -41,6 +41,7 @@
     boost::shared_ptr<DicomVolumeImage>  volume_;
     std::string                          instanceId_;
     std::string                          transferSyntaxUid_;
+    bool                                 pixelDataLoaded_;
 
     const std::string& GetInstanceId() const;
 
@@ -59,6 +60,13 @@
     OrthancMultiframeVolumeLoader(boost::shared_ptr<DicomVolumeImage> volume,
                                   IOracle& oracle,
                                   IObservable& oracleObservable);
+    
+    virtual ~OrthancMultiframeVolumeLoader();
+
+    bool IsPixelDataLoaded() const
+    {
+      return pixelDataLoaded_;
+    }
 
     void LoadInstance(const std::string& instanceId);
   };
--- a/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp	Fri Aug 02 17:38:31 2019 +0200
+++ b/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp	Fri Aug 02 17:38:44 2019 +0200
@@ -264,10 +264,14 @@
       if (quality == BEST_QUALITY)
       {
         std::auto_ptr<GetOrthancImageCommand> tmp(new GetOrthancImageCommand);
-        // TODO: review the following comment. Commented out by bgo on 2019-07-19
-        // reason: Alain has seen cases where gzipping the uint16 image took 11 sec 
-        // to produce 5mb. The unzipped request was much much faster.
-        //tmp->SetHttpHeader("Accept-Encoding", "gzip");
+        // TODO: review the following comment. 
+        // - Commented out by bgo on 2019-07-19 | reason: Alain has seen cases 
+        //   where gzipping the uint16 image took 11 sec to produce 5mb. 
+        //   The unzipped request was much much faster.
+        // - Re-enabled on 2019-07-30. Reason: in Web Assembly, the browser 
+        //   does not use the Accept-Encoding header and always requests
+        //   compression. Furthermore, NOT 
+        tmp->SetHttpHeader("Accept-Encoding", "gzip");
         tmp->SetHttpHeader("Accept", std::string(Orthanc::EnumerationToString(Orthanc::MimeType_Pam)));
         tmp->SetInstanceUri(instance, slice.GetExpectedPixelFormat());
         tmp->SetExpectedPixelFormat(slice.GetExpectedPixelFormat());
@@ -288,6 +292,12 @@
       command->SetPayload(new Orthanc::SingleValueObject<unsigned int>(sliceIndex));
       oracle_.Schedule(*this, command.release());
     }
+    else
+    {
+      // loading is finished!
+      volumeImageReadyInHighQuality_ = true;
+      BroadcastMessage(OrthancSeriesVolumeProgressiveLoader::VolumeImageReadyInHighQuality(*this));
+    }
   }
 
 /**
@@ -415,7 +425,8 @@
     active_(false),
     simultaneousDownloads_(4),
     volume_(volume),
-    sorter_(new BasicFetchingItemsSorter::Factory)
+    sorter_(new BasicFetchingItemsSorter::Factory),
+    volumeImageReadyInHighQuality_(false)
   {
     oracleObservable.RegisterObserverCallback(
       new Callable<OrthancSeriesVolumeProgressiveLoader, OrthancRestApiCommand::SuccessMessage>
@@ -430,6 +441,10 @@
       (*this, &OrthancSeriesVolumeProgressiveLoader::LoadJpegSliceContent));
   }
 
+  OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader()
+  {
+    LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::~OrthancSeriesVolumeProgressiveLoader()";
+  }
 
   void OrthancSeriesVolumeProgressiveLoader::SetSimultaneousDownloads(unsigned int count)
   {
@@ -450,8 +465,10 @@
 
   void OrthancSeriesVolumeProgressiveLoader::LoadSeries(const std::string& seriesId)
   {
+//    LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries seriesId=" << seriesId;
     if (active_)
     {
+//      LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries NOT ACTIVE! --> ERROR";
       throw Orthanc::OrthancException(Orthanc::ErrorCode_BadSequenceOfCalls);
     }
     else
@@ -461,7 +478,9 @@
       std::auto_ptr<OrthancRestApiCommand> command(new OrthancRestApiCommand);
       command->SetUri("/series/" + seriesId + "/instances-tags");
 
+//      LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries about to call oracle_.Schedule";
       oracle_.Schedule(*this, command.release());
+//      LOG(TRACE) << "OrthancSeriesVolumeProgressiveLoader::LoadSeries called oracle_.Schedule";
     }
   }
   
--- a/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h	Fri Aug 02 17:38:31 2019 +0200
+++ b/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.h	Fri Aug 02 17:38:44 2019 +0200
@@ -104,23 +104,34 @@
 
     void LoadJpegSliceContent(const GetOrthancWebViewerJpegCommand::SuccessMessage& message);
 
-    IOracle&                                       oracle_;
-    bool                                           active_;
-    unsigned int                                   simultaneousDownloads_;
-    SeriesGeometry                                 seriesGeometry_;
-    boost::shared_ptr<DicomVolumeImage>            volume_;
-    std::auto_ptr<IFetchingItemsSorter::IFactory>  sorter_;
-    std::auto_ptr<IFetchingStrategy>               strategy_;
-    std::vector<unsigned int>                      slicesQuality_;
+    IOracle&                                      oracle_;
+    bool                                          active_;
+    unsigned int                                  simultaneousDownloads_;
+    SeriesGeometry                                seriesGeometry_;
+    boost::shared_ptr<DicomVolumeImage>           volume_;
+    std::auto_ptr<IFetchingItemsSorter::IFactory> sorter_;
+    std::auto_ptr<IFetchingStrategy>              strategy_;
+    std::vector<unsigned int>                     slicesQuality_;
+    bool                                          volumeImageReadyInHighQuality_;
 
 
   public:
+    ORTHANC_STONE_DEFINE_ORIGIN_MESSAGE(__FILE__, __LINE__, VolumeImageReadyInHighQuality, OrthancSeriesVolumeProgressiveLoader);
+
+
     OrthancSeriesVolumeProgressiveLoader(const boost::shared_ptr<DicomVolumeImage>& volume,
                                          IOracle& oracle,
                                          IObservable& oracleObservable);
 
+    virtual ~OrthancSeriesVolumeProgressiveLoader();
+
     void SetSimultaneousDownloads(unsigned int count);
 
+    bool IsVolumeImageReadyInHighQuality() const
+    {
+      return volumeImageReadyInHighQuality_;
+    }
+
     void LoadSeries(const std::string& seriesId);
 
     /**
--- a/Framework/Oracle/WebAssemblyOracle.cpp	Fri Aug 02 17:38:31 2019 +0200
+++ b/Framework/Oracle/WebAssemblyOracle.cpp	Fri Aug 02 17:38:44 2019 +0200
@@ -153,6 +153,10 @@
        **/
       
       std::auto_ptr<FetchContext> context(reinterpret_cast<FetchContext*>(fetch->userData));
+      if (fetch->userData == NULL)
+      {
+        LOG(ERROR) << "WebAssemblyOracle::FetchContext::SuccessCallback fetch->userData is NULL!!!!!!!";
+      }
 
       std::string answer;
       if (fetch->numBytes > 0)
@@ -170,12 +174,14 @@
        **/
 
       HttpHeaders headers;
-      if (!context->GetExpectedContentType().empty())
+      if (fetch->userData != NULL)
       {
-        headers["Content-Type"] = context->GetExpectedContentType();
+        if (!context->GetExpectedContentType().empty())
+        {
+          headers["Content-Type"] = context->GetExpectedContentType();
+        }
       }
       
-      
       emscripten_fetch_close(fetch);
 
 
@@ -394,22 +400,53 @@
   void WebAssemblyOracle::Execute(const IObserver& receiver,
                                   OrthancRestApiCommand* command)
   {
-    FetchCommand fetch(*this, receiver, command);
+    try
+    {
+      //LOG(TRACE) << "*********** WebAssemblyOracle::Execute.";
+      //LOG(TRACE) << "WebAssemblyOracle::Execute | command = " << command;
+      FetchCommand fetch(*this, receiver, command);
+
+      fetch.SetMethod(command->GetMethod());
+      fetch.SetUri(command->GetUri());
+      fetch.SetHttpHeaders(command->GetHttpHeaders());
+      fetch.SetTimeout(command->GetTimeout());
+
+      if (command->GetMethod() == Orthanc::HttpMethod_Post ||
+        command->GetMethod() == Orthanc::HttpMethod_Put)
+      {
+        std::string body;
+        command->SwapBody(body);
+        fetch.SetBody(body);
+      }
 
-    fetch.SetMethod(command->GetMethod());
-    fetch.SetUri(command->GetUri());
-    fetch.SetHttpHeaders(command->GetHttpHeaders());
-    fetch.SetTimeout(command->GetTimeout());
-      
-    if (command->GetMethod() == Orthanc::HttpMethod_Post ||
-        command->GetMethod() == Orthanc::HttpMethod_Put)
+      fetch.Execute();
+      //LOG(TRACE) << "*********** successful end of WebAssemblyOracle::Execute.";
+    }
+    catch (const Orthanc::OrthancException& e)
     {
-      std::string body;
-      command->SwapBody(body);
-      fetch.SetBody(body);
+      if (e.HasDetails())
+      {
+        LOG(ERROR) << "OrthancException in WebAssemblyOracle::Execute: " << e.What() << " Details: " << e.GetDetails();
+      }
+      else
+      {
+        LOG(ERROR) << "OrthancException in WebAssemblyOracle::Execute: " << e.What();
+      }
+      //LOG(TRACE) << "*********** failing end of WebAssemblyOracle::Execute.";
+      throw;
     }
-
-    fetch.Execute();
+    catch (const std::exception& e)
+    {
+      LOG(ERROR) << "std::exception in WebAssemblyOracle::Execute: " << e.what();
+//       LOG(TRACE) << "*********** failing end of WebAssemblyOracle::Execute.";
+      throw;
+    }
+    catch (...)
+    {
+      LOG(ERROR) << "Unknown exception in WebAssemblyOracle::Execute";
+//       LOG(TRACE) << "*********** failing end of WebAssemblyOracle::Execute.";
+      throw;
+    }
   }
     
     
@@ -453,14 +490,36 @@
     switch (command->GetType())
     {
       case IOracleCommand::Type_OrthancRestApi:
+        //// DIAGNOSTIC. PLEASE REMOVE IF IT HAS BEEN COMMITTED BY MISTAKE
+        //{
+        //  const IObserver* pReceiver = &receiver;
+        //  LOG(TRACE) << "WebAssemblyOracle::Schedule | pReceiver is " << pReceiver;
+        //  LOG(TRACE) << "WebAssemblyOracle::Schedule | command = " << command;
+        //  OrthancRestApiCommand* rac = dynamic_cast<OrthancRestApiCommand*>(protection.get());
+        //  LOG(TRACE) << "WebAssemblyOracle::Schedule | typed command = " << rac;
+        //  LOG(TRACE) << "WebAssemblyOracle::Schedule" << rac->GetUri();
+        //}
+        //// END OF BLOCK TO REMOVE
         Execute(receiver, dynamic_cast<OrthancRestApiCommand*>(protection.release()));
         break;
         
       case IOracleCommand::Type_GetOrthancImage:
+        //// DIAGNOSTIC. PLEASE REMOVE IF IT HAS BEEN COMMITTED BY MISTAKE
+        //{
+        //  GetOrthancImageCommand* rac = dynamic_cast<GetOrthancImageCommand*>(protection.get());
+        //  LOG(TRACE) << "WebAssemblyOracle::Schedule" << rac->GetUri();
+        //}
+        //// END OF BLOCK TO REMOVE
         Execute(receiver, dynamic_cast<GetOrthancImageCommand*>(protection.release()));
         break;
 
       case IOracleCommand::Type_GetOrthancWebViewerJpeg:
+        //// DIAGNOSTIC. PLEASE REMOVE IF IT HAS BEEN COMMITTED BY MISTAKE
+        //{
+        //  GetOrthancWebViewerJpegCommand* rac = dynamic_cast<GetOrthancWebViewerJpegCommand*>(protection.get());
+        //  LOG(TRACE) << "WebAssemblyOracle::Schedule" << rac->GetUri();
+        //}
+        //// END OF BLOCK TO REMOVE
         Execute(receiver, dynamic_cast<GetOrthancWebViewerJpegCommand*>(protection.release()));
         break;          
             
--- a/Framework/Radiography/RadiographyScene.cpp	Fri Aug 02 17:38:31 2019 +0200
+++ b/Framework/Radiography/RadiographyScene.cpp	Fri Aug 02 17:38:44 2019 +0200
@@ -606,8 +606,8 @@
 
     Extent2D extent = GetSceneExtent();
 
-    int w = static_cast<int>(std::round(extent.GetWidth() / pixelSpacingX));
-    int h = static_cast<int>(std::round(extent.GetHeight() / pixelSpacingY));
+    int w = boost::math::iround(extent.GetWidth() / pixelSpacingX);
+    int h = boost::math::iround(extent.GetHeight() / pixelSpacingY);
 
     if (w < 0 || h < 0)
     {
--- a/Framework/Scene2D/Scene2D.cpp	Fri Aug 02 17:38:31 2019 +0200
+++ b/Framework/Scene2D/Scene2D.cpp	Fri Aug 02 17:38:44 2019 +0200
@@ -132,7 +132,7 @@
 
     if (found != content_.end())
     {
-      LOG(INFO) << "DeleteLayer --found-- (" << depth << ")";
+      LOG(TRACE) << "DeleteLayer --found-- (" << depth << ")";
       assert(found->second != NULL);
       delete found->second;
       content_.erase(found);
--- a/Framework/Volumes/DicomVolumeImageMPRSlicer.cpp	Fri Aug 02 17:38:31 2019 +0200
+++ b/Framework/Volumes/DicomVolumeImageMPRSlicer.cpp	Fri Aug 02 17:38:44 2019 +0200
@@ -100,7 +100,12 @@
   }
 
 
-  IVolumeSlicer::IExtractedSlice* 
+  DicomVolumeImageMPRSlicer::~DicomVolumeImageMPRSlicer()
+  {
+    LOG(TRACE) << "DicomVolumeImageMPRSlicer::~DicomVolumeImageMPRSlicer()";
+  }
+
+  IVolumeSlicer::IExtractedSlice*
   DicomVolumeImageMPRSlicer::ExtractSlice(const CoordinateSystem3D& cuttingPlane)
   {
     if (volume_->HasGeometry())
--- a/Framework/Volumes/DicomVolumeImageMPRSlicer.h	Fri Aug 02 17:38:31 2019 +0200
+++ b/Framework/Volumes/DicomVolumeImageMPRSlicer.h	Fri Aug 02 17:38:44 2019 +0200
@@ -89,6 +89,8 @@
     {
     }
 
+    virtual ~DicomVolumeImageMPRSlicer();
+
     virtual IExtractedSlice* ExtractSlice(const CoordinateSystem3D& cuttingPlane) ORTHANC_OVERRIDE;
   };
 }
--- a/Framework/Volumes/VolumeSceneLayerSource.cpp	Fri Aug 02 17:38:31 2019 +0200
+++ b/Framework/Volumes/VolumeSceneLayerSource.cpp	Fri Aug 02 17:38:44 2019 +0200
@@ -55,6 +55,10 @@
     }
   }
 
+  VolumeSceneLayerSource::~VolumeSceneLayerSource()
+  {
+    ClearLayer();
+  }
 
   void VolumeSceneLayerSource::RemoveConfigurator()
   {
--- a/Framework/Volumes/VolumeSceneLayerSource.h	Fri Aug 02 17:38:31 2019 +0200
+++ b/Framework/Volumes/VolumeSceneLayerSource.h	Fri Aug 02 17:38:44 2019 +0200
@@ -53,6 +53,8 @@
                            int layerDepth,
                            const boost::shared_ptr<IVolumeSlicer>& slicer);
 
+    ~VolumeSceneLayerSource();
+
     const IVolumeSlicer& GetSlicer() const
     {
       return *slicer_;
--- a/Resources/CMake/OrthancStoneConfiguration.cmake	Fri Aug 02 17:38:31 2019 +0200
+++ b/Resources/CMake/OrthancStoneConfiguration.cmake	Fri Aug 02 17:38:44 2019 +0200
@@ -410,6 +410,8 @@
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingItemsSorter.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/BasicFetchingStrategy.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/DicomStructureSetLoader.cpp
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderCache.h
+  ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderCache.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/LoaderStateMachine.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancMultiframeVolumeLoader.cpp
   ${ORTHANC_STONE_ROOT}/Framework/Loaders/OrthancSeriesVolumeProgressiveLoader.cpp
--- a/Samples/MultiPlatform/BasicScene/mainSdl.cpp	Fri Aug 02 17:38:31 2019 +0200
+++ b/Samples/MultiPlatform/BasicScene/mainSdl.cpp	Fri Aug 02 17:38:44 2019 +0200
@@ -183,7 +183,7 @@
     SdlOpenGLViewport viewport("Hello", 1024, 768);
     MessageBroker broker;
     boost::shared_ptr<UndoStack> undoStack(new UndoStack);
-    boost::shared_ptr<ViewportController> controller = boost::make_shared<ViewportController>(undoStack, boost::ref(broker), viewport);
+    boost::shared_ptr<ViewportController> controller = boost::make_shared<ViewportController>(undoStack, boost::ref(broker), boost::ref(viewport));
     interactor.reset(new BasicScene2DInteractor(controller));
     PrepareScene(controller->GetScene());
     Run(controller);
--- a/Samples/Sdl/FusionMprSdl.cpp	Fri Aug 02 17:38:31 2019 +0200
+++ b/Samples/Sdl/FusionMprSdl.cpp	Fri Aug 02 17:38:44 2019 +0200
@@ -103,7 +103,8 @@
   void FusionMprSdlApp::DisplayInfoText()
   {
     // do not try to use stuff too early!
-    if (compositor_.get() == NULL)
+    ICompositor* pCompositor = &(viewport_.GetCompositor());
+    if (pCompositor == NULL)
       return;
 
     std::stringstream msg;
@@ -116,10 +117,10 @@
 	std::string msgS = msg.str();
 
     TextSceneLayer* layerP = NULL;
-    if (GetScene()->HasLayer(FIXED_INFOTEXT_LAYER_ZINDEX))
+    if (GetScene().HasLayer(FIXED_INFOTEXT_LAYER_ZINDEX))
     {
       TextSceneLayer& layer = dynamic_cast<TextSceneLayer&>(
-        GetScene()->GetLayer(FIXED_INFOTEXT_LAYER_ZINDEX));
+        GetScene().GetLayer(FIXED_INFOTEXT_LAYER_ZINDEX));
       layerP = &layer;
     }
     else
@@ -131,29 +132,29 @@
       layer->SetBorder(20);
       layer->SetAnchor(BitmapAnchor_TopLeft);
       //layer->SetPosition(0,0);
-      GetScene()->SetLayer(FIXED_INFOTEXT_LAYER_ZINDEX, layer.release());
+      GetScene().SetLayer(FIXED_INFOTEXT_LAYER_ZINDEX, layer.release());
     }
     // position the fixed info text in the upper right corner
     layerP->SetText(msgS.c_str());
-    double cX = compositor_->GetCanvasWidth() * (-0.5);
-    double cY = compositor_->GetCanvasHeight() * (-0.5);
-    GetScene()->GetCanvasToSceneTransform().Apply(cX,cY);
+    double cX = viewport_.GetCompositor().GetCanvasWidth() * (-0.5);
+    double cY = viewport_.GetCompositor().GetCanvasHeight() * (-0.5);
+    GetScene().GetCanvasToSceneTransform().Apply(cX,cY);
     layerP->SetPosition(cX, cY);
   }
 
   void FusionMprSdlApp::DisplayFloatingCtrlInfoText(const PointerEvent& e)
   {
-    ScenePoint2D p = e.GetMainPosition().Apply(GetScene()->GetCanvasToSceneTransform());
+    ScenePoint2D p = e.GetMainPosition().Apply(GetScene().GetCanvasToSceneTransform());
 
     char buf[128];
     sprintf(buf, "S:(%0.02f,%0.02f) C:(%0.02f,%0.02f)", 
       p.GetX(), p.GetY(), 
       e.GetMainPosition().GetX(), e.GetMainPosition().GetY());
 
-    if (GetScene()->HasLayer(FLOATING_INFOTEXT_LAYER_ZINDEX))
+    if (GetScene().HasLayer(FLOATING_INFOTEXT_LAYER_ZINDEX))
     {
       TextSceneLayer& layer =
-        dynamic_cast<TextSceneLayer&>(GetScene()->GetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX));
+        dynamic_cast<TextSceneLayer&>(GetScene().GetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX));
       layer.SetText(buf);
       layer.SetPosition(p.GetX(), p.GetY());
     }
@@ -165,13 +166,13 @@
       layer->SetBorder(20);
       layer->SetAnchor(BitmapAnchor_BottomCenter);
       layer->SetPosition(p.GetX(), p.GetY());
-      GetScene()->SetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX, layer.release());
+      GetScene().SetLayer(FLOATING_INFOTEXT_LAYER_ZINDEX, layer.release());
     }
   }
 
   void FusionMprSdlApp::HideInfoText()
   {
-    GetScene()->DeleteLayer(FLOATING_INFOTEXT_LAYER_ZINDEX);
+    GetScene().DeleteLayer(FLOATING_INFOTEXT_LAYER_ZINDEX);
   }
 
   void FusionMprSdlApp::HandleApplicationEvent(
@@ -191,7 +192,7 @@
         // The "left-ctrl" key is down, while no tracker is present
         // Let's display the info text
         PointerEvent e;
-        e.AddPosition(compositor_->GetPixelCenterCoordinates(
+        e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(
           event.button.x, event.button.y));
 
         DisplayFloatingCtrlInfoText(e);
@@ -204,7 +205,7 @@
         {
           //LOG(TRACE) << "(activeTracker_.get() != NULL)";
           PointerEvent e;
-          e.AddPosition(compositor_->GetPixelCenterCoordinates(
+          e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(
             event.button.x, event.button.y));
           
           //LOG(TRACE) << "event.button.x = " << event.button.x << "     " <<
@@ -223,7 +224,7 @@
       if (activeTracker_)
       {
         PointerEvent e;
-        e.AddPosition(compositor_->GetPixelCenterCoordinates(event.button.x, event.button.y));
+        e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(event.button.x, event.button.y));
         activeTracker_->PointerUp(e);
         if (!activeTracker_->IsAlive())
           activeTracker_.reset();
@@ -232,7 +233,7 @@
     else if (event.type == SDL_MOUSEBUTTONDOWN)
     {
       PointerEvent e;
-      e.AddPosition(compositor_->GetPixelCenterCoordinates(
+      e.AddPosition(controller_->GetViewport().GetPixelCenterCoordinates(
         event.button.x, event.button.y));
       if (activeTracker_)
       {
@@ -270,8 +271,8 @@
         }
         break;
       case SDLK_s:
-        controller_->FitContent(compositor_->GetCanvasWidth(),
-          compositor_->GetCanvasHeight());
+        controller_->FitContent(viewport_.GetCompositor().GetCanvasWidth(),
+          viewport_.GetCompositor().GetCanvasHeight());
         break;
 
       case SDLK_z:
@@ -309,8 +310,8 @@
       case SDLK_c:
         TakeScreenshot(
           "screenshot.png",
-          compositor_->GetCanvasWidth(),
-          compositor_->GetCanvasHeight());
+          viewport_.GetCompositor().GetCanvasWidth(),
+          viewport_.GetCompositor().GetCanvasHeight());
         break;
 
       default:
@@ -340,7 +341,7 @@
 
     case SDL_BUTTON_RIGHT:
       return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker
-        (controller_, e, compositor_->GetCanvasHeight()));
+        (controller_, e, viewport_.GetCompositor().GetCanvasHeight()));
 
     case SDL_BUTTON_LEFT:
     {
@@ -372,7 +373,7 @@
             controller_, e));
         case FusionMprGuiTool_Zoom:
           return boost::shared_ptr<IFlexiblePointerTracker>(new ZoomSceneTracker(
-            controller_, e, compositor_->GetCanvasHeight()));
+            controller_, e, viewport_.GetCompositor().GetCanvasHeight()));
         //case GuiTool_AngleMeasure:
         //  return new AngleMeasureTracker(GetScene(), e);
         //case GuiTool_CircleMeasure:
@@ -409,6 +410,7 @@
     , oracle_(*this)
     , currentTool_(FusionMprGuiTool_Rotate)
     , undoStack_(new UndoStack)
+    , viewport_("Hello", 1024, 1024, false) // False means we do NOT let Windows treat this as a legacy application that needs to be scaled
   {
     //oracleObservable.RegisterObserverCallback
     //(new Callable
@@ -427,7 +429,7 @@
       <FusionMprSdlApp, OracleCommandExceptionMessage>(*this, &FusionMprSdlApp::Handle));
     
     controller_ = boost::shared_ptr<ViewportController>(
-      new ViewportController(undoStack_, broker_));
+      new ViewportController(undoStack_, broker_, viewport_));
 
     controller_->RegisterObserverCallback(
       new Callable<FusionMprSdlApp, ViewportController::SceneTransformChanged>
@@ -466,7 +468,7 @@
       p[4] = 0;
       p[5] = 0;
 
-      GetScene()->SetLayer(TEXTURE_2x2_1_ZINDEX, new ColorTextureSceneLayer(i));
+      GetScene().SetLayer(TEXTURE_2x2_1_ZINDEX, new ColorTextureSceneLayer(i));
     }
   }
 
@@ -483,7 +485,7 @@
     unsigned int canvasWidth,
     unsigned int canvasHeight)
   {
-    CairoCompositor compositor(*GetScene(), canvasWidth, canvasHeight);
+    CairoCompositor compositor(GetScene(), canvasWidth, canvasHeight);
     compositor.SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT, FONT_SIZE_0, Orthanc::Encoding_Latin1);
     compositor.Refresh();
 
@@ -557,7 +559,7 @@
     const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
     OrthancStone::ILayerStyleConfigurator* style)
   {
-    source1_.reset(new OrthancStone::VolumeSceneLayerSource(*controller_->GetScene(), depth, volume));
+    source1_.reset(new OrthancStone::VolumeSceneLayerSource(controller_->GetScene(), depth, volume));
 
     if (style != NULL)
     {
@@ -569,7 +571,7 @@
     const boost::shared_ptr<OrthancStone::IVolumeSlicer>& volume,
     OrthancStone::ILayerStyleConfigurator* style)
   {
-    source2_.reset(new OrthancStone::VolumeSceneLayerSource(*controller_->GetScene(), depth, volume));
+    source2_.reset(new OrthancStone::VolumeSceneLayerSource(controller_->GetScene(), depth, volume));
 
     if (style != NULL)
     {
@@ -580,25 +582,21 @@
   void FusionMprSdlApp::SetStructureSet(int depth,
     const boost::shared_ptr<OrthancStone::DicomStructureSetLoader>& volume)
   {
-    source3_.reset(new OrthancStone::VolumeSceneLayerSource(*controller_->GetScene(), depth, volume));
+    source3_.reset(new OrthancStone::VolumeSceneLayerSource(controller_->GetScene(), depth, volume));
   }
   
   void FusionMprSdlApp::Run()
   {
     // False means we do NOT let Windows treat this as a legacy application
     // that needs to be scaled
-    SdlOpenGLContext window("Hello", 1024, 1024, false);
-
-    controller_->FitContent(window.GetCanvasWidth(), window.GetCanvasHeight());
+    controller_->FitContent(viewport_.GetCanvasWidth(), viewport_.GetCanvasHeight());
 
     glEnable(GL_DEBUG_OUTPUT);
     glDebugMessageCallback(OpenGLMessageCallback, 0);
 
-    compositor_.reset(new OpenGLCompositor(window, *GetScene()));
-
-    compositor_->SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
+    viewport_.GetCompositor().SetFont(0, Orthanc::EmbeddedResources::UBUNTU_FONT,
       FONT_SIZE_0, Orthanc::Encoding_Latin1);
-    compositor_->SetFont(1, Orthanc::EmbeddedResources::UBUNTU_FONT,
+    viewport_.GetCompositor().SetFont(1, Orthanc::EmbeddedResources::UBUNTU_FONT,
       FONT_SIZE_1, Orthanc::Encoding_Latin1);
 
 
@@ -675,7 +673,7 @@
 
     while (!g_stopApplication)
     {
-      compositor_->Refresh();
+      viewport_.GetCompositor().Refresh();
 
 //////// from loader
       if (source1_.get() != NULL)
@@ -713,12 +711,11 @@
           switch (event.key.keysym.sym)
           {
           case SDLK_f:
-            window.GetWindow().ToggleMaximize();
+            viewport_.GetWindow().ToggleMaximize();
             break;
 
           case SDLK_s:
-            controller_->FitContent(
-              window.GetCanvasWidth(), window.GetCanvasHeight());
+            controller_->FitContent(viewport_.GetCanvasWidth(), viewport_.GetCanvasHeight());
             break;
 
           case SDLK_q:
@@ -733,10 +730,6 @@
       SDL_Delay(1);
     }
 
-    // the following is paramount because the compositor holds a reference
-    // to the scene and we do not want this reference to become dangling
-    compositor_.reset(NULL);
-
     //// from loader
 
     //Orthanc::SystemToolbox::ServerBarrier();
--- a/Samples/Sdl/FusionMprSdl.h	Fri Aug 02 17:38:31 2019 +0200
+++ b/Samples/Sdl/FusionMprSdl.h	Fri Aug 02 17:38:44 2019 +0200
@@ -18,6 +18,8 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  **/
 
+#include "../../Framework/Viewport/SdlViewport.h"
+
 #include "../../Framework/Messages/IObserver.h"
 #include "../../Framework/Messages/IMessageEmitter.h"
 #include "../../Framework/Oracle/OracleCommandExceptionMessage.h"
@@ -41,7 +43,7 @@
   class ThreadedOracle;
   class VolumeSceneLayerSource;
   class NativeFusionMprApplicationContext;
-
+  class SdlOpenGLViewport;
    
   enum FusionMprGuiTool
   {
@@ -80,8 +82,8 @@
     void SetInfoDisplayMessage(std::string key, std::string value);
     void DisableTracker();
 
-    boost::shared_ptr<Scene2D> GetScene();
-    boost::shared_ptr<const Scene2D> GetScene() const;
+    Scene2D&       GetScene();
+    const Scene2D& GetScene() const;
 
     void HandleApplicationEvent(const SDL_Event& event);
 
@@ -174,7 +176,6 @@
 
     boost::shared_ptr<VolumeSceneLayerSource>  source1_, source2_, source3_;
 
-    std::auto_ptr<OpenGLCompositor> compositor_;
     /**
     WARNING: the measuring tools do store a reference to the scene, and it
     paramount that the scene gets destroyed AFTER the measurement tools.
@@ -196,7 +197,7 @@
 
     FusionMprGuiTool currentTool_;
     boost::shared_ptr<UndoStack> undoStack_;
-
+    SdlOpenGLViewport viewport_;
   };
 
 }
--- a/Samples/Sdl/TrackerSampleApp.cpp	Fri Aug 02 17:38:31 2019 +0200
+++ b/Samples/Sdl/TrackerSampleApp.cpp	Fri Aug 02 17:38:44 2019 +0200
@@ -641,7 +641,7 @@
 
   static bool g_stopApplication = false;
   
-  OpenGLCompositor& TrackerSampleApp::GetCompositor()
+  ICompositor& TrackerSampleApp::GetCompositor()
   {
     using namespace Orthanc;
     try
@@ -655,7 +655,7 @@
      }
   }
 
-  const OpenGLCompositor& TrackerSampleApp::GetCompositor() const
+  const ICompositor& TrackerSampleApp::GetCompositor() const
   {
     using namespace Orthanc;
     try
@@ -705,7 +705,7 @@
           switch (event.key.keysym.sym)
           {
           case SDLK_f:
-            viewport_.GetContext().GetWindow().ToggleMaximize();
+            viewport_.GetWindow().ToggleMaximize();
             break;
 
           case SDLK_q:
--- a/Samples/Sdl/TrackerSampleApp.h	Fri Aug 02 17:38:31 2019 +0200
+++ b/Samples/Sdl/TrackerSampleApp.h	Fri Aug 02 17:38:44 2019 +0200
@@ -84,12 +84,12 @@
     In the case of this app, the viewport is an SDL viewport and it has 
     a OpenGLCompositor& GetCompositor() method
     */
-    OpenGLCompositor& GetCompositor();
+    ICompositor& GetCompositor();
 
     /**
     See the other overload
     */
-    const OpenGLCompositor& GetCompositor() const;
+    const ICompositor& GetCompositor() const;
 
     /**
     This returns a random point in the canvas part of the scene, but in