From 3cf0d85bd779206edbf58f172f55864807901952 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20M=C3=BCller?= <thomas94@gmx.net>
Date: Thu, 29 Dec 2022 12:18:19 +0100
Subject: [PATCH] Generic file loading: drag a dataset, network config, or
 snapshot into the GUI

Can also pass datasets, network configs, or snapshots as positional arguments now.
---
 CMakeLists.txt                               |   1 +
 README.md                                    |  27 +-
 docs/nerf_dataset_tips.md                    |   4 +-
 include/neural-graphics-primitives/common.h  |  20 +-
 include/neural-graphics-primitives/testbed.h |  13 +-
 notebooks/instant_ngp.ipynb                  |  10 +-
 scripts/run.py                               |  94 +++---
 scripts/scenes.py                            |  24 +-
 src/common.cu                                |  79 +++++
 src/main.cu                                  | 107 ++-----
 src/nerf_loader.cu                           |   4 -
 src/python_api.cu                            |  13 +-
 src/testbed.cu                               | 310 ++++++++++++++-----
 src/testbed_image.cu                         |   6 +-
 14 files changed, 429 insertions(+), 283 deletions(-)
 create mode 100644 src/common.cu

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 469d619..4b3a4ba 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -205,6 +205,7 @@ endif()
 list(APPEND NGP_SOURCES
 	${GUI_SOURCES}
 	src/camera_path.cu
+	src/common.cu
 	src/common_device.cu
 	src/marching_cubes.cu
 	src/nerf_loader.cu
diff --git a/README.md b/README.md
index 2f91650..54fcd05 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,7 @@ To get started with NVIDIA Instant NeRF, check out the [blog post](https://devel
 
 For business inquiries, please submit the [NVIDIA research licensing form](https://www.nvidia.com/en-us/research/inquiries/).
 
+
 ## Windows binary release
 
 If you have Windows and if you do not need Python bindings, you can download one of the following binary releases and then jump directly to the [usage instructions](https://github.com/NVlabs/instant-ngp#interactive-training-and-rendering). These releases are automatically regenerated whenever the code gets updated, so you can be sure that they have the latest features.
@@ -26,6 +27,7 @@ If you have Windows and if you do not need Python bindings, you can download one
 
 If you use Linux, or want the Python bindings, or if your GPU is not listed above (e.g. Hopper, Volta, or Maxwell generations), use the following step-by-step instructions to compile __instant-ngp__ yourself.
 
+
 ## Requirements
 
 - An __NVIDIA GPU__; tensor cores increase performance when available. All shown results come from an RTX 3090.
@@ -37,7 +39,7 @@ If you use Linux, or want the Python bindings, or if your GPU is not listed abov
   - __Linux:__ CUDA 10.2 or higher
 - __[CMake](https://cmake.org/) v3.21 or higher__.
 - __(optional) [Python](https://www.python.org/) 3.7 or higher__ for interactive bindings. Also, run `pip install -r requirements.txt`.
-- __(optional) [OptiX](https://developer.nvidia.com/optix) 7.3 or higher__ for faster mesh SDF training.
+- __(optional) [OptiX](https://developer.nvidia.com/optix) 7.6 or higher__ for faster mesh SDF training.
 - __(optional) [Vulkan SDK](https://vulkan.lunarg.com/)__ for DLSS support.
 
 
@@ -49,7 +51,7 @@ sudo apt-get install build-essential git python3-dev python3-pip libopenexr-dev
 
 Alternatively, if you are using Arch or Arch derivatives, install the following packages
 ```sh
-sudo pacman -S base-devel cmake openexr libxi glfw openmp libxinerama libxcursor
+sudo pacman -S cuda base-devel cmake openexr libxi glfw openmp libxinerama libxcursor
 ```
 
 We also recommend installing [CUDA](https://developer.nvidia.com/cuda-toolkit) and [OptiX](https://developer.nvidia.com/optix) in `/usr/local/` and adding the CUDA installation to your PATH.
@@ -60,11 +62,6 @@ export PATH="/usr/local/cuda-11.4/bin:$PATH"
 export LD_LIBRARY_PATH="/usr/local/cuda-11.4/lib64:$LD_LIBRARY_PATH"
 ```
 
-For Arch and derivatives,
-```sh
-sudo pacman -S cuda
-```
-
 
 ## Compilation (Windows & Linux)
 
@@ -112,13 +109,13 @@ Let's start using __instant-ngp__; more information about the GUI and other scri
 One test scene is provided in this repository, using a small number of frames from a casually captured phone video:
 
 ```sh
-instant-ngp$ ./build/instant-ngp --scene data/nerf/fox
+instant-ngp$ ./build/instant-ngp data/nerf/fox
 ```
 
 On Windows you need to reverse the slashes here (and below), i.e.:
 
 ```sh
-instant-ngp> .\build\instant-ngp --scene data\nerf\fox
+instant-ngp> .\build\instant-ngp data\nerf\fox
 ```
 
 <img src="docs/assets_readme/fox.png"/>
@@ -127,7 +124,7 @@ Alternatively, download any NeRF-compatible scene (e.g. from the [NeRF authors'
 Now you can run:
 
 ```sh
-instant-ngp$ ./build/instant-ngp --scene data/nerf_synthetic/lego/transforms_train.json
+instant-ngp$ ./build/instant-ngp data/nerf_synthetic/lego/transforms_train.json
 ```
 
 **[To prepare your own dataset for use with our NeRF implementation, click here.](docs/nerf_dataset_tips.md)** See also [this video](https://www.youtube.com/watch?v=8GbENSmdVeE) for a guided walkthrough.
@@ -135,7 +132,7 @@ instant-ngp$ ./build/instant-ngp --scene data/nerf_synthetic/lego/transforms_tra
 ### SDF armadillo
 
 ```sh
-instant-ngp$ ./build/instant-ngp --scene data/sdf/armadillo.obj
+instant-ngp$ ./build/instant-ngp data/sdf/armadillo.obj
 ```
 
 <img src="docs/assets_readme/armadillo.png"/>
@@ -143,7 +140,7 @@ instant-ngp$ ./build/instant-ngp --scene data/sdf/armadillo.obj
 ### Image of Einstein
 
 ```sh
-instant-ngp$ ./build/instant-ngp --scene data/image/albert.exr
+instant-ngp$ ./build/instant-ngp data/image/albert.exr
 ```
 
 <img src="docs/assets_readme/albert.png"/>
@@ -151,7 +148,7 @@ instant-ngp$ ./build/instant-ngp --scene data/image/albert.exr
 To reproduce the gigapixel results, download, for example, [the Tokyo image](https://www.flickr.com/photos/trevor_dobson_inefekt69/29314390837) and convert it to `.bin` using the `scripts/convert_image.py` script. This custom format improves compatibility and loading speed when resolution is high. Now you can run:
 
 ```sh
-instant-ngp$ ./build/instant-ngp --scene data/image/tokyo.bin
+instant-ngp$ ./build/instant-ngp data/image/tokyo.bin
 ```
 
 
@@ -160,7 +157,7 @@ instant-ngp$ ./build/instant-ngp --scene data/image/tokyo.bin
 Download the [nanovdb volume for the Disney cloud](https://drive.google.com/drive/folders/1SuycSAOSG64k2KLV7oWgyNWyCvZAkafK?usp=sharing), which is derived [from here](https://disneyanimation.com/data-sets/?drawer=/resources/clouds/) ([CC BY-SA 3.0](https://media.disneyanimation.com/uploads/production/data_set_asset/6/asset/License_Cloud.pdf)).
 
 ```sh
-instant-ngp$ ./build/instant-ngp --mode volume --scene data/volume/wdas_cloud_quarter.nvdb
+instant-ngp$ ./build/instant-ngp data/volume/wdas_cloud_quarter.nvdb
 ```
 <img src="docs/assets_readme/cloud.png"/>
 
@@ -204,7 +201,7 @@ For an example of how the `./build/instant-ngp` application can be implemented a
 Here is a typical command line using `scripts/run.py` to generate a 5-second flythrough of the fox dataset to the (default) file `video.mp4`, after using the GUI to save a (default) NeRF snapshot `base.msgpack` and a set of camera key frames: (see [this video](https://www.youtube.com/watch?v=8GbENSmdVeE) for a guided walkthrough)
 
 ```sh
-instant-ngp$ python scripts/run.py --mode nerf --scene data/nerf/fox --load_snapshot data/nerf/fox/base.msgpack --video_camera_path data/nerf/fox/base_cam.json --video_n_seconds 5 --video_fps 60 --width 1920 --height 1080
+instant-ngp$ python scripts/run.py data/nerf/fox/base.msgpack --video_camera_path data/nerf/fox/base_cam.json --video_n_seconds 5 --video_fps 60 --width 1920 --height 1080
 ```
 
 If you'd rather build new models from the hash encoding and fast neural networks, consider the [__tiny-cuda-nn__'s PyTorch extension](https://github.com/nvlabs/tiny-cuda-nn#pytorch-extension).
diff --git a/docs/nerf_dataset_tips.md b/docs/nerf_dataset_tips.md
index 47de07a..7952082 100644
--- a/docs/nerf_dataset_tips.md
+++ b/docs/nerf_dataset_tips.md
@@ -84,7 +84,7 @@ The `aabb_scale` parameter is the most important `instant-ngp` specific paramete
 Assuming success, you can now train your NeRF model as follows, starting in the `instant-ngp` folder:
 
 ```sh
-instant-ngp$ ./build/instant-ngp --mode nerf --scene [path to training data folder containing transforms.json]
+instant-ngp$ ./build/instant-ngp [path to training data folder containing transforms.json]
 ```
 
 ### Record3D
@@ -102,7 +102,7 @@ With an >=iPhone 12 Pro, one can use [Record3D](https://record3d.app/) to collec
 
 5. Launch Instant-NGP training:
 	```
-	./build/instant-ngp --scene path/to/data
+	./build/instant-ngp path/to/data
 	```
 
 ## Tips for NeRF training data
diff --git a/include/neural-graphics-primitives/common.h b/include/neural-graphics-primitives/common.h
index 9b5c0cf..30c59cd 100644
--- a/include/neural-graphics-primitives/common.h
+++ b/include/neural-graphics-primitives/common.h
@@ -53,8 +53,17 @@
 #include <chrono>
 #include <functional>
 
+#if defined(__NVCC__) || (defined(__clang__) && defined(__CUDA__))
+#define NGP_HOST_DEVICE __host__ __device__
+#else
+#define NGP_HOST_DEVICE
+#endif
+
 NGP_NAMESPACE_BEGIN
 
+bool ends_with(const std::string& str, const std::string& ending);
+bool ends_with_case_insensitive(const std::string& str, const std::string& ending);
+
 using Vector2i32 = Eigen::Matrix<uint32_t, 2, 1>;
 using Vector3i16 = Eigen::Matrix<uint16_t, 3, 1>;
 using Vector4i16 = Eigen::Matrix<uint16_t, 4, 1>;
@@ -155,8 +164,13 @@ enum class ETestbedMode : int {
 	Sdf,
 	Image,
 	Volume,
+	None,
 };
 
+ETestbedMode mode_from_scene(const std::string& scene);
+ETestbedMode mode_from_string(const std::string& str);
+std::string to_string(ETestbedMode);
+
 enum class ESDFGroundTruthMode : int {
 	RaytracedMesh,
 	SpheretracedMesh,
@@ -185,12 +199,6 @@ struct Lens {
 	float params[7] = {};
 };
 
-#if defined(__NVCC__) || (defined(__clang__) && defined(__CUDA__))
-#define NGP_HOST_DEVICE __host__ __device__
-#else
-#define NGP_HOST_DEVICE
-#endif
-
 inline NGP_HOST_DEVICE float sign(float x) {
 	return copysignf(1.0, x);
 }
diff --git a/include/neural-graphics-primitives/testbed.h b/include/neural-graphics-primitives/testbed.h
index 8e55eeb..1efa2b2 100644
--- a/include/neural-graphics-primitives/testbed.h
+++ b/include/neural-graphics-primitives/testbed.h
@@ -61,7 +61,7 @@ class GLTexture;
 class Testbed {
 public:
 	EIGEN_MAKE_ALIGNED_OPERATOR_NEW
-	Testbed(ETestbedMode mode);
+	Testbed(ETestbedMode mode = ETestbedMode::None);
 	~Testbed();
 	Testbed(ETestbedMode mode, const std::string& data_path) : Testbed(mode) { load_training_data(data_path); }
 	Testbed(ETestbedMode mode, const std::string& data_path, const std::string& network_config_path) : Testbed(mode, data_path) { reload_network_from_file(network_config_path); }
@@ -69,6 +69,8 @@ public:
 	void load_training_data(const std::string& data_path);
 	void clear_training_data();
 
+	void set_mode(ETestbedMode mode);
+
 	using distance_fun_t = std::function<void(uint32_t, const Eigen::Vector3f*, float*, cudaStream_t)>;
 	using normals_fun_t = std::function<void(uint32_t, const Eigen::Vector3f*, Eigen::Vector3f*, cudaStream_t)>;
 
@@ -284,8 +286,9 @@ public:
 	void render_image(CudaRenderBuffer& render_buffer, cudaStream_t stream);
 	void render_frame(const Eigen::Matrix<float, 3, 4>& camera_matrix0, const Eigen::Matrix<float, 3, 4>& camera_matrix1, const Eigen::Vector4f& nerf_rolling_shutter, CudaRenderBuffer& render_buffer, bool to_srgb = true) ;
 	void visualize_nerf_cameras(ImDrawList* list, const Eigen::Matrix<float, 4, 4>& world2proj);
+	filesystem::path find_network_config(const filesystem::path& network_config_path);
 	nlohmann::json load_network_config(const filesystem::path& network_config_path);
-	void reload_network_from_file(const std::string& network_config_path);
+	void reload_network_from_file(const std::string& network_config_path = "");
 	void reload_network_from_json(const nlohmann::json& json, const std::string& config_base_path=""); // config_base_path is needed so that if the passed in json uses the 'parent' feature, we know where to look... be sure to use a filename, or if a directory, end with a trailing slash
 	void reset_accumulation(bool due_to_camera_movement = false, bool immediate_redraw = true);
 	void redraw_next_frame() {
@@ -306,7 +309,7 @@ public:
 	void translate_camera(const Eigen::Vector3f& rel);
 	void mouse_drag(const Eigen::Vector2f& rel, int button);
 	void mouse_wheel(Eigen::Vector2f m, float delta);
-	void handle_file(const std::string& file);
+	void load_file(const std::string& file);
 	void set_nerf_camera_matrix(const Eigen::Matrix<float, 3, 4>& cam);
 	Eigen::Vector3f look_at() const;
 	void set_look_at(const Eigen::Vector3f& pos);
@@ -463,7 +466,7 @@ public:
 	bool m_training_data_available = false;
 	bool m_render = true;
 	int m_max_spp = 0;
-	ETestbedMode m_testbed_mode = ETestbedMode::Sdf;
+	ETestbedMode m_testbed_mode = ETestbedMode::None;
 	bool m_max_level_rand_training = false;
 
 	// Rendering stuff
@@ -844,7 +847,7 @@ public:
 	bool m_train_network = true;
 
 	filesystem::path m_data_path;
-	filesystem::path m_network_config_path;
+	filesystem::path m_network_config_path = "base.json";
 
 	nlohmann::json m_network_config;
 
diff --git a/notebooks/instant_ngp.ipynb b/notebooks/instant_ngp.ipynb
index 5559011..4d2cda7 100644
--- a/notebooks/instant_ngp.ipynb
+++ b/notebooks/instant_ngp.ipynb
@@ -5847,7 +5847,7 @@
       "source": [
         "train_steps = 2000  #@param {type:\"integer\"}\n",
         "snapshot_path = os.path.join(scene_path, f\"{train_steps}.msgpack\")\n",
-        "!python ./scripts/run.py --scene {scene_path} --mode nerf --n_steps {train_steps} --save_snapshot {snapshot_path}"
+        "!python ./scripts/run.py {scene_path} --n_steps {train_steps} --save_snapshot {snapshot_path}"
       ]
     },
     {
@@ -5863,7 +5863,7 @@
         "\n",
         "Example command:\n",
         "```\n",
-        "./build/instant-ngp --scene data/nerf/fox --no-train --snapshot /data/nerf/fox/2000.msgpack\n",
+        "./build/instant-ngp /data/nerf/fox/2000.msgpack\n",
         "```\n",
         "\n",
         "After you're done, **upload `base_cam.json` to the root folder of your scene.**"
@@ -5999,7 +5999,7 @@
         "height = 720 #@param {type:\"integer\"}\n",
         "output_video_path = os.path.join(scene_path, \"output_video.mp4\")\n",
         "\n",
-        "!python scripts/run.py --mode nerf --scene {scene_path} --load_snapshot {snapshot_path} --video_camera_path {video_camera_path} --video_n_seconds 2 --video_fps 25 --width 720 --height 720 --video_output {output_video_path}\n",
+        "!python scripts/run.py {snapshot_path} --video_camera_path {video_camera_path} --video_n_seconds 2 --video_fps 25 --width 720 --height 720 --video_output {output_video_path}\n",
         "print(f\"Generated video saved to:\\n{output_video_path}\")"
       ]
     }
@@ -6018,11 +6018,11 @@
     },
     "language_info": {
       "name": "python",
-      "version": "3.10.5 (tags/v3.10.5:f377153, Jun  6 2022, 16:14:13) [MSC v.1929 64 bit (AMD64)]"
+      "version": "3.9.7 (tags/v3.9.7:1016ef3, Aug 30 2021, 20:19:38) [MSC v.1929 64 bit (AMD64)]"
     },
     "vscode": {
       "interpreter": {
-        "hash": "80f0ca567e8a8332be8d0227e77114b80c729e82298f4777b19db59a6217bb0d"
+        "hash": "76392a4a51364e66be8d9e8c24551b51109eb4003b2256c2b9df7166893b45c1"
       }
     }
   },
diff --git a/scripts/run.py b/scripts/run.py
index 56636db..6b4929a 100644
--- a/scripts/run.py
+++ b/scripts/run.py
@@ -27,8 +27,10 @@ import pyngp as ngp # noqa
 def parse_args():
 	parser = argparse.ArgumentParser(description="Run instant neural graphics primitives with additional configuration & output options")
 
-	parser.add_argument("--scene", "--training_data", default="", help="The scene to load. Can be the scene's name or a full path to the training data.")
-	parser.add_argument("--mode", default="", const="nerf", nargs="?", choices=["nerf", "sdf", "image", "volume"], help="Mode can be 'nerf', 'sdf', 'image' or 'volume'. Inferred from the scene if unspecified.")
+	parser.add_argument("files", nargs="*", help="Files to be loaded. Can be a scene, network config, snapshot, camera path, or a combination of those.")
+
+	parser.add_argument("--scene", "--training_data", default="", help="The scene to load. Can be the scene's name or a full path to the training data. Can be NeRF dataset, a *.obj/*.ply mesh for training a SDF, an image, or a *.nvdb volume.")
+	parser.add_argument("--mode", default="", type=str, help=argparse.SUPPRESS) # deprecated
 	parser.add_argument("--network", default="", help="Path to the network config. Uses the scene's default if unspecified.")
 
 	parser.add_argument("--load_snapshot", default="", help="Load this snapshot before training. recommended extension: .msgpack")
@@ -68,70 +70,53 @@ def parse_args():
 
 	return parser.parse_args()
 
+def get_scene(scene):
+	for scenes in [scenes_sdf, scenes_nerf, scenes_image, scenes_volume]:
+		if scene in scenes:
+			return scenes[scene]
+	return None
+
 if __name__ == "__main__":
 	args = parse_args()
 
-	args.mode = args.mode or mode_from_scene(args.scene) or mode_from_scene(args.load_snapshot)
-	if not args.mode:
-		raise ValueError("Must specify either a valid '--mode' or '--scene' argument.")
-
-	if args.mode == "sdf":
-		mode = ngp.TestbedMode.Sdf
-		configs_dir = os.path.join(ROOT_DIR, "configs", "sdf")
-		scenes = scenes_sdf
-	elif args.mode == "nerf":
-		mode = ngp.TestbedMode.Nerf
-		configs_dir = os.path.join(ROOT_DIR, "configs", "nerf")
-		scenes = scenes_nerf
-	elif args.mode == "image":
-		mode = ngp.TestbedMode.Image
-		configs_dir = os.path.join(ROOT_DIR, "configs", "image")
-		scenes = scenes_image
-	elif args.mode == "volume":
-		mode = ngp.TestbedMode.Volume
-		configs_dir = os.path.join(ROOT_DIR, "configs", "volume")
-		scenes = scenes_volume
-	else:
-		raise ValueError("Must specify either a valid '--mode' or '--scene' argument.")
-
-	base_network = os.path.join(configs_dir, "base.json")
-	if args.scene in scenes:
-		network = scenes[args.scene]["network"] if "network" in scenes[args.scene] else "base"
-		base_network = os.path.join(configs_dir, network+".json")
-	network = args.network if args.network else base_network
-	if not os.path.isabs(network):
-		network = os.path.join(configs_dir, network)
-
-	testbed = ngp.Testbed(mode)
-	testbed.nerf.sharpen = float(args.sharpen)
-	testbed.exposure = args.exposure
-	if mode == ngp.TestbedMode.Sdf:
-		testbed.tonemap_curve = ngp.TonemapCurve.ACES
+	if args.mode:
+		print("Warning: the '--mode' argument is no longer in use. It has no effect. The mode is automatically chosen based on the scene.")
+
+	testbed = ngp.Testbed()
+
+	for file in args.files:
+		scene_info = get_scene(file)
+		if scene_info:
+			file = os.path.join(scene_info["data_dir"], scene_info["dataset"])
+		testbed.load_file(file)
 
 	if args.scene:
-		scene = args.scene
-		if not os.path.exists(args.scene) and args.scene in scenes:
-			scene = os.path.join(scenes[args.scene]["data_dir"], scenes[args.scene]["dataset"])
-		testbed.load_training_data(scene)
+		scene_info = get_scene(args.scene)
+		if scene_info is not None:
+			args.scene = os.path.join(scene_info["data_dir"], scene_info["dataset"])
+			if not args.network and "network" in scene_info:
+				args.network = scene_info["network"]
+
+		testbed.load_training_data(args.scene)
 
 	if args.gui:
 		# Pick a sensible GUI resolution depending on arguments.
 		sw = args.width or 1920
 		sh = args.height or 1080
-		while sw*sh > 1920*1080*4:
+		while sw * sh > 1920 * 1080 * 4:
 			sw = int(sw / 2)
 			sh = int(sh / 2)
 		testbed.init_window(sw, sh, second_window = args.second_window or False)
 
 
 	if args.load_snapshot:
-		snapshot = args.load_snapshot
-		if not os.path.exists(snapshot) and snapshot in scenes:
-			snapshot = default_snapshot_filename(scenes[snapshot])
-		print("Loading snapshot ", snapshot)
-		testbed.load_snapshot(snapshot)
-	else:
-		testbed.reload_network_from_file(network)
+		scene_info = get_scene(args.load_snapshot)
+		if scene_info is not None:
+			args.load_snapshot = default_snapshot_filename(scene_info)
+		print("Loading snapshot ", args.load_snapshot)
+		testbed.load_snapshot(args.load_snapshot)
+	elif args.network:
+		testbed.reload_network_from_file(args.network)
 
 	ref_transforms = {}
 	if args.screenshot_transforms: # try to load the given file straight away
@@ -139,13 +124,18 @@ if __name__ == "__main__":
 		with open(args.screenshot_transforms) as f:
 			ref_transforms = json.load(f)
 
+	if testbed.mode == ngp.TestbedMode.Sdf:
+		testbed.tonemap_curve = ngp.TonemapCurve.ACES
+
+	testbed.nerf.sharpen = float(args.sharpen)
+	testbed.exposure = args.exposure
 	testbed.shall_train = args.train if args.gui else True
 
 
 	testbed.nerf.render_with_lens_distortion = True
 
-	network_stem = os.path.splitext(os.path.basename(network))[0]
-	if args.mode == "sdf":
+	network_stem = os.path.splitext(os.path.basename(args.network))[0] if args.network else "base"
+	if testbed.mode == ngp.TestbedMode.Sdf:
 		setup_colored_sdf(testbed, args.scene)
 
 	if args.near_distance >= 0.0:
diff --git a/scripts/scenes.py b/scripts/scenes.py
index a96f60b..ca9e30c 100644
--- a/scripts/scenes.py
+++ b/scripts/scenes.py
@@ -12,7 +12,6 @@ import os
 
 from common import *
 
-
 def ours_real_converted(path, frameidx):
 	return {
 		"data_dir"      : os.path.join(NERF_DATA_FOLDER, path),
@@ -74,7 +73,6 @@ scenes_nerf = {
 	"stump"   : mipnerf_360("stump", frameidx=0),
 }
 
-
 def ours_mesh(name, up = [0,1,0], infolder=True):
 	return {
 		"data_dir"      : os.path.join(SDF_DATA_FOLDER, f"{name}") if infolder else SDF_DATA_FOLDER,
@@ -86,7 +84,6 @@ scenes_sdf = {
 	"armadillo"     : ours_mesh("armadillo", infolder=False),
 }
 
-
 def ours_image(name, infolder=True):
 	data_dir = os.path.join(IMAGE_DATA_FOLDER, f"{name}") if infolder else IMAGE_DATA_FOLDER
 	dataset = f"{name}.bin"
@@ -107,7 +104,6 @@ scenes_image = {
 }
 
 
-
 def ours_volume(name, ds):
 	return {
 		"data_dir" : os.path.join(VOLUME_DATA_FOLDER, f"{name}"),
@@ -225,20 +221,8 @@ def setup_colored_sdf(testbed, scene, softshadow=True):
 	testbed.sdf.shadow_sharpness = 16 if softshadow else 2048
 	testbed.scale = testbed.scale * 1.13
 
-def default_snapshot_filename(scene):
+def default_snapshot_filename(scene_info):
 	filename = "base.msgpack"
-	if scene["dataset"]:
-		filename = f"{os.path.splitext(scene['dataset'])[0]}_{filename}"
-	return os.path.join(scene["data_dir"], filename)
-
-def mode_from_scene(scene):
-	if scene in scenes_sdf:
-		return "sdf"
-	elif scene in scenes_nerf:
-		return "nerf"
-	elif scene in scenes_image:
-		return "image"
-	elif scene in scenes_volume:
-		return "volume"
-	else:
-		return ""
+	if scene_info["dataset"]:
+		filename = f"{os.path.splitext(scene_info['dataset'])[0]}_{filename}"
+	return os.path.join(scene_info["data_dir"], filename)
diff --git a/src/common.cu b/src/common.cu
new file mode 100644
index 0000000..e9538ce
--- /dev/null
+++ b/src/common.cu
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2022, NVIDIA CORPORATION.  All rights reserved.
+ *
+ * NVIDIA CORPORATION and its licensors retain all intellectual property
+ * and proprietary rights in and to this software, related documentation
+ * and any modifications thereto.  Any use, reproduction, disclosure or
+ * distribution of this software and related documentation without an express
+ * license agreement from NVIDIA CORPORATION is strictly prohibited.
+ */
+
+/** @file   common_device.cu
+ *  @author Thomas Müller, NVIDIA
+ */
+
+#include <neural-graphics-primitives/common.h>
+
+#include <tiny-cuda-nn/common.h>
+
+#include <filesystem/path.h>
+
+using namespace tcnn;
+namespace fs = filesystem;
+
+NGP_NAMESPACE_BEGIN
+
+bool ends_with(const std::string& str, const std::string& ending) {
+	if (ending.length() > str.length()) {
+		return false;
+	}
+	return std::equal(std::rbegin(ending), std::rend(ending), std::rbegin(str));
+}
+
+bool ends_with_case_insensitive(const std::string& str, const std::string& ending) {
+	return ends_with(to_lower(str), to_lower(ending));
+}
+
+ETestbedMode mode_from_scene(const std::string& scene) {
+	fs::path scene_path = scene;
+	if (!scene_path.exists()) {
+		return ETestbedMode::None;
+	}
+
+	if (scene_path.is_directory() || equals_case_insensitive(scene_path.extension(), "json")) {
+		return ETestbedMode::Nerf;
+	} else if (equals_case_insensitive(scene_path.extension(), "obj") || equals_case_insensitive(scene_path.extension(), "stl")) {
+		return ETestbedMode::Sdf;
+	} else if (equals_case_insensitive(scene_path.extension(), "nvdb")) {
+		return ETestbedMode::Volume;
+	} else { // probably an image. Too bothersome to list all supported ones: exr, bin, jpg, png, tga, hdr, ...
+		return ETestbedMode::Image;
+	}
+}
+
+ETestbedMode mode_from_string(const std::string& str) {
+	if (equals_case_insensitive(str, "nerf")) {
+		return ETestbedMode::Nerf;
+	} else if (equals_case_insensitive(str, "sdf")) {
+		return ETestbedMode::Sdf;
+	} else if (equals_case_insensitive(str, "image")) {
+		return ETestbedMode::Image;
+	} else if (equals_case_insensitive(str, "volume")) {
+		return ETestbedMode::Volume;
+	} else {
+		return ETestbedMode::None;
+	}
+}
+
+std::string to_string(ETestbedMode mode) {
+	switch (mode) {
+		case ETestbedMode::Nerf: return "nerf";
+		case ETestbedMode::Sdf: return "sdf";
+		case ETestbedMode::Image: return "image";
+		case ETestbedMode::Volume: return "volume";
+		case ETestbedMode::None: return "none";
+		default: throw std::runtime_error{fmt::format("Can not convert mode {} to string.", (int)mode)};
+	}
+}
+
+NGP_NAMESPACE_END
diff --git a/src/main.cu b/src/main.cu
index 480d3af..ee39a56 100644
--- a/src/main.cu
+++ b/src/main.cu
@@ -43,7 +43,7 @@ int main(int argc, char** argv) {
 	ValueFlag<string> mode_flag{
 		parser,
 		"MODE",
-		"Mode can be 'nerf', 'sdf', or 'image' or 'volume'. Inferred from the scene if unspecified.",
+		"Deprecated. Do not use.",
 		{'m', "mode"},
 	};
 
@@ -71,7 +71,7 @@ int main(int argc, char** argv) {
 	ValueFlag<string> scene_flag{
 		parser,
 		"SCENE",
-		"The scene to load. Can be NeRF dataset, a *.obj mesh for training a SDF, an image, or a *.nvdb volume.",
+		"The scene to load. Can be NeRF dataset, a *.obj/*.ply mesh for training a SDF, an image, or a *.nvdb volume.",
 		{'s', "scene"},
 	};
 
@@ -103,6 +103,12 @@ int main(int argc, char** argv) {
 		{'v', "version"},
 	};
 
+	PositionalList<string> files{
+		parser,
+		"files",
+		"Files to be loaded. Can be a scene, network config, snapshot, camera path, or a combination of those.",
+	};
+
 	// Parse command line arguments and react to parsing
 	// errors using exceptions.
 	try {
@@ -126,99 +132,32 @@ int main(int argc, char** argv) {
 	}
 
 	try {
-		ETestbedMode mode;
-		if (!mode_flag) {
-			if (!scene_flag) {
-				tlog::error() << "Must specify either a mode or a scene";
-				return 1;
-			}
-
-			fs::path scene_path = get(scene_flag);
-			if (!scene_path.exists()) {
-				tlog::error() << "Scene path " << scene_path << " does not exist.";
-				return 1;
-			}
-
-			if (scene_path.is_directory() || equals_case_insensitive(scene_path.extension(), "json")) {
-				mode = ETestbedMode::Nerf;
-			} else if (equals_case_insensitive(scene_path.extension(), "obj") || equals_case_insensitive(scene_path.extension(), "stl")) {
-				mode = ETestbedMode::Sdf;
-			} else if (equals_case_insensitive(scene_path.extension(), "nvdb")) {
-				mode = ETestbedMode::Volume;
-			} else {
-				mode = ETestbedMode::Image;
-			}
-		} else {
-			auto mode_str = get(mode_flag);
-			if (equals_case_insensitive(mode_str, "nerf")) {
-				mode = ETestbedMode::Nerf;
-			} else if (equals_case_insensitive(mode_str, "sdf")) {
-				mode = ETestbedMode::Sdf;
-			} else if (equals_case_insensitive(mode_str, "image")) {
-				mode = ETestbedMode::Image;
-			} else if (equals_case_insensitive(mode_str, "volume")) {
-				mode = ETestbedMode::Volume;
-			} else {
-				tlog::error() << "Mode must be one of 'nerf', 'sdf', 'image', and 'volume'.";
-				return 1;
-			}
+		if (mode_flag) {
+			tlog::warning() << "The '--mode' argument is no longer in use. It has no effect. The mode is automatically chosen based on the scene.";
 		}
 
-		Testbed testbed{mode};
+		Testbed testbed;
 
-		if (scene_flag) {
-			fs::path scene_path = get(scene_flag);
-			if (!scene_path.exists()) {
-				tlog::error() << "Scene path " << scene_path << " does not exist.";
-				return 1;
-			}
-			testbed.load_training_data(scene_path.str());
+		for (auto file : get(files)) {
+			testbed.load_file(file);
 		}
 
-		std::string mode_str;
-		switch (mode) {
-			case ETestbedMode::Nerf:   mode_str = "nerf";   break;
-			case ETestbedMode::Sdf:    mode_str = "sdf";    break;
-			case ETestbedMode::Image:  mode_str = "image";  break;
-			case ETestbedMode::Volume: mode_str = "volume"; break;
+		if (scene_flag) {
+			testbed.load_training_data(get(scene_flag));
 		}
 
 		if (snapshot_flag) {
-			// Load network from a snapshot if one is provided
-			fs::path snapshot_path = get(snapshot_flag);
-			if (!snapshot_path.exists()) {
-				tlog::error() << "Snapshot path " << snapshot_path << " does not exist.";
-				return 1;
-			}
-
-			testbed.load_snapshot(snapshot_path.str());
-			testbed.m_train = false;
-		} else {
-			// Otherwise, load the network config and prepare for training
-			fs::path network_config_path = fs::path{"configs"}/mode_str;
-			if (network_config_flag) {
-				auto network_config_str = get(network_config_flag);
-				if ((network_config_path/network_config_str).exists()) {
-					network_config_path = network_config_path/network_config_str;
-				} else {
-					network_config_path = network_config_str;
-				}
-			} else {
-				network_config_path = network_config_path/"base.json";
-			}
-
-			if (!network_config_path.exists()) {
-				tlog::error() << "Network config path " << network_config_path << " does not exist.";
-				return 1;
-			}
-
-			testbed.reload_network_from_file(network_config_path.str());
-			testbed.m_train = !no_train_flag;
+			testbed.load_snapshot(get(snapshot_flag));
+		} else if (network_config_flag) {
+			testbed.reload_network_from_file(get(network_config_flag));
 		}
 
+		testbed.m_train = !no_train_flag;
+
+#ifdef NGP_GUI
 		bool gui = !no_gui_flag;
-#ifndef NGP_GUI
-		gui = false;
+#else
+		bool gui = false;
 #endif
 
 		if (gui) {
diff --git a/src/nerf_loader.cu b/src/nerf_loader.cu
index eb0555b..003c423 100644
--- a/src/nerf_loader.cu
+++ b/src/nerf_loader.cu
@@ -168,10 +168,6 @@ __global__ void compute_sharpness(Eigen::Vector2i sharpness_resolution, Eigen::V
 	*sharpness_data = (variance_of_laplacian) ; // / max(0.00001f,tot_lum*tot_lum); // var / (tot+0.001f);
 }
 
-bool ends_with(const std::string& str, const std::string& suffix) {
-	return str.size() >= suffix.size() && 0 == str.compare(str.size()-suffix.size(), suffix.size(), suffix);
-}
-
 NerfDataset create_empty_nerf_dataset(size_t n_images, int aabb_scale, bool is_hdr) {
 	NerfDataset result{};
 	result.n_images = n_images;
diff --git a/src/python_api.cu b/src/python_api.cu
index d6785ba..8d4b27a 100644
--- a/src/python_api.cu
+++ b/src/python_api.cu
@@ -237,8 +237,12 @@ PYBIND11_MODULE(pyngp, m) {
 		.value("Sdf", ETestbedMode::Sdf)
 		.value("Image", ETestbedMode::Image)
 		.value("Volume", ETestbedMode::Volume)
+		.value("None", ETestbedMode::None)
 		.export_values();
 
+	m.def("mode_from_scene", &mode_from_scene);
+	m.def("mode_from_string", &mode_from_string);
+
 	py::enum_<EGroundTruthRenderMode>(m, "GroundTruthRenderMode")
 		.value("Shade", EGroundTruthRenderMode::Shade)
 		.value("Depth", EGroundTruthRenderMode::Depth)
@@ -336,9 +340,10 @@ PYBIND11_MODULE(pyngp, m) {
 
 	py::class_<Testbed> testbed(m, "Testbed");
 	testbed
-		.def(py::init<ETestbedMode>())
+		.def(py::init<ETestbedMode>(), py::arg("mode") = ETestbedMode::None)
 		.def(py::init<ETestbedMode, const std::string&, const std::string&>())
 		.def(py::init<ETestbedMode, const std::string&, const json&>())
+		.def_readonly("mode", &Testbed::m_testbed_mode)
 		.def("create_empty_nerf_dataset", &Testbed::create_empty_nerf_dataset, "Allocate memory for a nerf dataset with a given size", py::arg("n_images"), py::arg("aabb_scale")=1, py::arg("is_hdr")=false)
 		.def("load_training_data", &Testbed::load_training_data, py::call_guard<py::gil_scoped_release>(), "Load training data from a given path.")
 		.def("clear_training_data", &Testbed::clear_training_data, "Clears training data to free up GPU memory.")
@@ -404,6 +409,7 @@ PYBIND11_MODULE(pyngp, m) {
 		.def("save_snapshot", &Testbed::save_snapshot, py::arg("path"), py::arg("include_optimizer_state")=false, "Save a snapshot of the currently trained model")
 		.def("load_snapshot", &Testbed::load_snapshot, py::arg("path"), "Load a previously saved snapshot")
 		.def("load_camera_path", &Testbed::load_camera_path, "Load a camera path", py::arg("path"))
+		.def("load_file", &Testbed::load_file, "Load a file and automatically determine how to handle it. Can be a snapshot, dataset, network config, or camera path.", py::arg("path"))
 		.def_property("loop_animation", &Testbed::loop_animation, &Testbed::set_loop_animation)
 		.def("compute_and_save_png_slices", &Testbed::compute_and_save_png_slices,
 			py::arg("filename"),
@@ -434,10 +440,7 @@ PYBIND11_MODULE(pyngp, m) {
 			"`thresh` is the density threshold; use 0 for SDF; 2.5 works well for NeRF. "
 			"If the aabb parameter specifies an inside-out (\"empty\") box (default), the current render_aabb bounding box is used."
 		)
-		;
-
-	// Interesting members.
-	testbed
+		// Interesting members.
 		.def_readwrite("dynamic_res", &Testbed::m_dynamic_res)
 		.def_readwrite("dynamic_res_target_fps", &Testbed::m_dynamic_res_target_fps)
 		.def_readwrite("fixed_res_factor", &Testbed::m_fixed_res_factor)
diff --git a/src/testbed.cu b/src/testbed.cu
index de8a921..e70f3c9 100644
--- a/src/testbed.cu
+++ b/src/testbed.cu
@@ -85,18 +85,24 @@ json merge_parent_network_config(const json &child, const fs::path &child_filena
 	return parent;
 }
 
-static bool ends_with(const std::string& str, const std::string& ending) {
-	if (ending.length() > str.length()) {
-		return false;
+void Testbed::load_training_data(const std::string& data_path_str) {
+	fs::path data_path = data_path_str;
+	if (!data_path.exists()) {
+		throw std::runtime_error{fmt::format("Data path '{}' does not exist.", data_path.str())};
+	}
+
+	// Automatically determine the mode from the first scene that's loaded
+	ETestbedMode scene_mode = mode_from_scene(data_path.str());
+	if (scene_mode == ETestbedMode::None) {
+		throw std::runtime_error{fmt::format("Unknown scene format for path '{}'.", data_path.str())};
 	}
-	return std::equal(std::rbegin(ending), std::rend(ending), std::rbegin(str));
-}
 
-void Testbed::load_training_data(const std::string& data_path) {
+	set_mode(scene_mode);
+
 	m_data_path = data_path;
 
 	if (!m_data_path.exists()) {
-		throw std::runtime_error{fmt::format("Data path {} does not exist.", m_data_path.str())};
+		throw std::runtime_error{fmt::format("Data path '{}' does not exist.", m_data_path.str())};
 	}
 
 	switch (m_testbed_mode) {
@@ -115,17 +121,68 @@ void Testbed::clear_training_data() {
 	m_nerf.training.dataset.metadata.clear();
 }
 
-json Testbed::load_network_config(const fs::path& network_config_path) {
-	if (!network_config_path.empty()) {
-		m_network_config_path = network_config_path;
+void Testbed::set_mode(ETestbedMode mode) {
+	if (mode == m_testbed_mode) {
+		return;
+	}
+
+	// Reset mode-specific members
+	m_image = {};
+	m_mesh = {};
+	m_nerf = {};
+	m_sdf = {};
+	m_volume = {};
+
+	// Kill training-related things
+	m_encoding = {};
+	m_loss = {};
+	m_network = {};
+	m_nerf_network = {};
+	m_optimizer = {};
+	m_trainer = {};
+	m_envmap = {};
+	m_distortion = {};
+	m_training_data_available = false;
+
+	// Reset paths that might be attached to the chosen mode
+	m_data_path = {};
+
+	m_testbed_mode = mode;
+
+	reset_camera();
+}
+
+fs::path Testbed::find_network_config(const fs::path& network_config_path) {
+	if (network_config_path.exists()) {
+		return network_config_path;
+	}
+
+	// The following resolution steps do not work if the path is absolute. Treat it as nonexistent.
+	if (network_config_path.is_absolute()) {
+		return network_config_path;
+	}
+
+	fs::path candidate = fs::path{"configs"}/to_string(m_testbed_mode)/network_config_path;
+	if (candidate.exists()) {
+		return candidate;
+	}
+
+	candidate = fs::path{"../"}/candidate;
+	if (candidate.exists()) {
+		return candidate;
 	}
 
-	tlog::info() << "Loading network config from: " << network_config_path;
+	return network_config_path;
+}
 
+json Testbed::load_network_config(const fs::path& network_config_path) {
+	bool is_snapshot = equals_case_insensitive(network_config_path.extension(), "msgpack");
 	if (network_config_path.empty() || !network_config_path.exists()) {
-		throw std::runtime_error{fmt::format("Network config {} does not exist.", network_config_path.str())};
+		throw std::runtime_error{fmt::format("Network {} '{}' does not exist.", is_snapshot ? "snapshot" : "config", network_config_path.str())};
 	}
 
+	tlog::info() << "Loading network " << (is_snapshot ? "snapshot" : "config") << " from: " << network_config_path;
+
 	json result;
 	if (equals_case_insensitive(network_config_path.extension(), "json")) {
 		std::ifstream f{network_config_path.str()};
@@ -140,13 +197,38 @@ json Testbed::load_network_config(const fs::path& network_config_path) {
 	return result;
 }
 
-void Testbed::reload_network_from_file(const std::string& network_config_path) {
-	if (!network_config_path.empty()) {
-		m_network_config_path = network_config_path;
+void Testbed::reload_network_from_file(const std::string& network_config_path_string) {
+	if (!network_config_path_string.empty()) {
+		fs::path candidate = find_network_config(network_config_path_string);
+		if (candidate.exists()) {
+			// Store the path _argument_ in the member variable. E.g. for the base config,
+			// it'll store `base.json`, even though the loaded config will be
+			// config/<mode>/base.json. This has the benefit of switching to the
+			// appropriate config when switching modes.
+			m_network_config_path = network_config_path_string;
+		}
 	}
 
-	m_network_config = load_network_config(m_network_config_path);
-	reset_network();
+	// If the testbed mode hasn't been decided yet, don't load a network yet, but
+	// still keep track of the requested config (see above).
+	if (m_testbed_mode == ETestbedMode::None) {
+		return;
+	}
+
+	fs::path full_network_config_path = find_network_config(m_network_config_path);
+	bool is_snapshot = equals_case_insensitive(full_network_config_path.extension(), "msgpack");
+
+	if (!full_network_config_path.exists()) {
+		tlog::warning() << "Network " << (is_snapshot ? "snapshot" : "config") << " path '" << full_network_config_path << "' does not exist.";
+	} else {
+		m_network_config = load_network_config(full_network_config_path);
+	}
+
+	// Reset training if we haven't loaded a snapshot of an already trained model, in which case, presumably the network
+	// configuration changed and the user is interested in seeing how it trains from scratch.
+	if (!is_snapshot) {
+		reset_network();
+	}
 }
 
 void Testbed::reload_network_from_json(const json& json, const std::string& config_base_path) {
@@ -156,36 +238,55 @@ void Testbed::reload_network_from_json(const json& json, const std::string& conf
 	reset_network();
 }
 
-void Testbed::handle_file(const std::string& file) {
-	if (ends_with(file, ".msgpack")) {
-		load_snapshot(file);
-	}
-	else if (ends_with(file, ".json")) {
-		reload_network_from_file(file);
-	} else if (ends_with(file, ".obj") || ends_with(file, ".stl")) {
-		m_data_path = file;
-		m_testbed_mode = ETestbedMode::Sdf;
-		load_mesh();
-	} else if (ends_with(file, ".exr") || ends_with(file, ".bin")) {
-		m_data_path = file;
-		m_testbed_mode = ETestbedMode::Image;
-		try {
-			load_image();
-		} catch (std::runtime_error& e) {
-			tlog::error() << "Failed to open image: " << e.what();
+void Testbed::load_file(const std::string& file_path) {
+	if (!fs::path{file_path}.exists()) {
+		// If the path doesn't exist, but a network config can be resolved, load that.
+		if (ends_with_case_insensitive(file_path, ".json") && find_network_config(file_path).exists()) {
+			reload_network_from_file(file_path);
+			return;
+		}
+
+		tlog::error() << "File '" << file_path << "' does not exist.";
+		return;
+	}
+
+	if (ends_with_case_insensitive(file_path, ".msgpack")) {
+		load_snapshot(file_path);
+		return;
+	}
+
+	// If we get a json file, we need to parse it to determine its purpose.
+	if (ends_with_case_insensitive(file_path, ".json")) {
+		json file;
+		{
+			std::ifstream f{file_path};
+			file = json::parse(f, nullptr, true, true);
+		}
+
+		// Snapshot in json format... inefficient, but technically supported.
+		if (file.contains("snapshot")) {
+			load_snapshot(file_path);
 			return;
 		}
-	} else if (ends_with(file, ".nvdb")) {
-		m_data_path = file;
-		m_testbed_mode = ETestbedMode::Volume;
-		try {
-			load_volume();
-		} catch (std::runtime_error& e) {
-			tlog::error() << "Failed to open volume: " << e.what();
+
+		// Regular network config
+		if (file.contains("parent") || file.contains("network") || file.contains("encoding") || file.contains("loss") || file.contains("optimizer")) {
+			reload_network_from_file(file_path);
+			return;
+		}
+
+		// Camera path
+		if (file.contains("path")) {
+			load_camera_path(file_path);
 			return;
 		}
-	} else {
-		tlog::error() << "Tried to open unknown file type: " << file;
+	}
+
+	// If the dragged file isn't any of the above, assume that it's training data
+	try {
+		load_training_data(file_path);
+	} catch (std::runtime_error& e) {
+		tlog::error() << "Failed to load training data: " << e.what();
 	}
 }
 
@@ -529,7 +630,7 @@ void Testbed::imgui() {
 			ImGui::Checkbox("Quaternion format", &export_extrinsics_in_quat_format);
 		}
 		if (imgui_colored_button("Reset training", 0.f)) {
-			reload_network_from_file("");
+			reload_network_from_file();
 		}
 		ImGui::SameLine();
 		ImGui::DragInt("Seed", (int*)&m_seed, 1.0f, 0, std::numeric_limits<int>::max());
@@ -1306,23 +1407,33 @@ bool Testbed::keyboard_event() {
 		set_exposure(m_exposure + (shift ? -0.5f : 0.5f));
 		redraw_next_frame();
 	}
+
 	if (ImGui::IsKeyPressed('R')) {
 		if (shift) {
 			reset_camera();
 		} else {
-			reload_network_from_file("");
+			reload_network_from_file();
 		}
 	}
-	if (ImGui::IsKeyPressed('O')) {
-		m_nerf.training.render_error_overlay = !m_nerf.training.render_error_overlay;
-	}
-	if (ImGui::IsKeyPressed('G')) {
-		m_render_ground_truth = !m_render_ground_truth;
-		reset_accumulation();
-		if (m_render_ground_truth) {
-			m_nerf.training.view = find_best_training_view(m_nerf.training.view);
+
+	if (m_training_data_available) {
+		if (ImGui::IsKeyPressed('O')) {
+			m_nerf.training.render_error_overlay = !m_nerf.training.render_error_overlay;
+		}
+
+		if (ImGui::IsKeyPressed('G')) {
+			m_render_ground_truth = !m_render_ground_truth;
+			reset_accumulation();
+			if (m_render_ground_truth) {
+				m_nerf.training.view = find_best_training_view(m_nerf.training.view);
+			}
+		}
+
+		if (ImGui::IsKeyPressed('T')) {
+			set_train(!m_train);
 		}
 	}
+
 	if (ImGui::IsKeyPressed('.')) {
 		if (m_single_view) {
 			if (m_visualized_dimension == m_network->width(m_visualized_layer)-1 && m_visualized_layer < m_network->num_forward_activations()-1) {
@@ -1335,6 +1446,7 @@ bool Testbed::keyboard_event() {
 			set_visualized_layer(std::max(0, std::min((int)m_network->num_forward_activations()-1, m_visualized_layer+1)));
 		}
 	}
+
 	if (ImGui::IsKeyPressed(',')) {
 		if (m_single_view) {
 			if (m_visualized_dimension == 0 && m_visualized_layer > 0) {
@@ -1347,14 +1459,14 @@ bool Testbed::keyboard_event() {
 			set_visualized_layer(std::max(0, std::min((int)m_network->num_forward_activations()-1, m_visualized_layer-1)));
 		}
 	}
+
 	if (ImGui::IsKeyPressed('M')) {
 		m_single_view = !m_single_view;
 		set_visualized_dim(-1);
 		reset_accumulation();
 	}
-	if (ImGui::IsKeyPressed('T')) {
-		set_train(!m_train);
-	}
+
+
 	if (ImGui::IsKeyPressed('N')) {
 		m_sdf.analytic_normals = !m_sdf.analytic_normals;
 		reset_accumulation();
@@ -1389,29 +1501,37 @@ bool Testbed::keyboard_event() {
 	if (ImGui::IsKeyDown('W')) {
 		translate_vec.z() += 1.0f;
 	}
+
 	if (ImGui::IsKeyDown('A')) {
 		translate_vec.x() += -1.0f;
 	}
+
 	if (ImGui::IsKeyDown('S')) {
 		translate_vec.z() += -1.0f;
 	}
+
 	if (ImGui::IsKeyDown('D')) {
 		translate_vec.x() += 1.0f;
 	}
+
 	if (ImGui::IsKeyDown(' ')) {
 		translate_vec.y() += -1.0f;
 	}
+
 	if (ImGui::IsKeyDown('C')) {
 		translate_vec.y() += 1.0f;
 	}
+
 	translate_vec *= m_camera_velocity * m_frame_ms.val() / 1000.0f;
 	if (shift) {
 		translate_vec *= 5;
 	}
+
 	if (translate_vec != Vector3f::Zero()) {
 		m_fps_camera = true;
 		translate_camera(translate_vec);
 	}
+
 	return false;
 }
 
@@ -1661,6 +1781,15 @@ void Testbed::train_and_render(bool skip_rendering) {
 		train(m_training_batch_size);
 	}
 
+	// If we don't have a trainer, as can happen when having loaded training data or changed modes without having
+	// explicitly loaded a new neural network.
+	if (m_testbed_mode != ETestbedMode::None && !m_network) {
+		reload_network_from_file();
+		if (!m_network) {
+			throw std::runtime_error{"Unable to reload neural network."};
+		}
+	}
+
 	if (m_mesh.optimize_mesh) {
 		optimise_mesh_step(1);
 	}
@@ -1905,14 +2034,7 @@ void Testbed::init_window(int resw, int resh, bool hidden, bool second_window) {
 	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
 	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
 	glfwWindowHint(GLFW_VISIBLE, hidden ? GLFW_FALSE : GLFW_TRUE);
-	std::string title = "Instant Neural Graphics Primitives v" NGP_VERSION " (";
-	switch (m_testbed_mode) {
-		case ETestbedMode::Image: title += "Image"; break;
-		case ETestbedMode::Sdf: title += "SDF"; break;
-		case ETestbedMode::Nerf: title += "NeRF"; break;
-		case ETestbedMode::Volume: title += "Volume"; break;
-	}
-	title += ")";
+	std::string title = "Instant Neural Graphics Primitives";
 	m_glfw_window = glfwCreateWindow(m_window_res.x(), m_window_res.y(), title.c_str(), NULL, NULL);
 	if (m_glfw_window == NULL) {
 		throw std::runtime_error{"GLFW window could not be created."};
@@ -1939,7 +2061,7 @@ void Testbed::init_window(int resw, int resh, bool hidden, bool second_window) {
 
 		testbed->redraw_gui_next_frame();
 		for (int i = 0; i < count; i++) {
-			testbed->handle_file(paths[i]);
+			testbed->load_file(paths[i]);
 		}
 	});
 
@@ -2117,8 +2239,8 @@ fs::path Testbed::training_data_path() const {
 }
 
 bool Testbed::want_repl() {
-	bool b=m_want_repl;
-	m_want_repl=false;
+	bool b = m_want_repl;
+	m_want_repl = false;
 	return b;
 }
 
@@ -2275,6 +2397,11 @@ void Testbed::reset_network(bool clear_density_grid) {
 
 	m_nerf.training.reset_camera_extrinsics();
 
+	if (clear_density_grid) {
+		m_nerf.density_grid.memset(0);
+		m_nerf.density_grid_bitfield.memset(0);
+	}
+
 	m_loss_graph_samples = 0;
 
 	// Default config
@@ -2425,17 +2552,17 @@ void Testbed::reset_network(bool clear_density_grid) {
 				tcnn::string_to_interpolation_type(encoding_config.value("interpolation", "linear"))
 			));
 
-			m_network = std::make_shared<NetworkWithInputEncoding<precision_t>>(m_encoding, dims.n_output, network_config);
 			m_sdf.uses_takikawa_encoding = true;
 		} else {
 			m_encoding.reset(create_encoding<precision_t>(dims.n_input, encoding_config));
-			m_network = std::make_shared<NetworkWithInputEncoding<precision_t>>(m_encoding, dims.n_output, network_config);
+
 			m_sdf.uses_takikawa_encoding = false;
 			if (m_sdf.octree_depth_target == 0 && encoding_config.contains("n_levels")) {
 				m_sdf.octree_depth_target = encoding_config["n_levels"];
 			}
 		}
 
+		m_network = std::make_shared<NetworkWithInputEncoding<precision_t>>(m_encoding, dims.n_output, network_config);
 		n_encoding_params = m_encoding->n_params();
 
 		tlog::info()
@@ -2472,15 +2599,9 @@ void Testbed::reset_network(bool clear_density_grid) {
 		}
 	}
 
-	if (clear_density_grid) {
-		m_nerf.density_grid.memset(0);
-		m_nerf.density_grid_bitfield.memset(0);
-	}
 }
 
-Testbed::Testbed(ETestbedMode mode)
-: m_testbed_mode(mode)
-{
+Testbed::Testbed(ETestbedMode mode) {
 	if (!(__CUDACC_VER_MAJOR__ > 10 || (__CUDACC_VER_MAJOR__ == 10 && __CUDACC_VER_MINOR__ >= 2))) {
 		throw std::runtime_error{"Testbed required CUDA 10.2 or later."};
 	}
@@ -2544,11 +2665,12 @@ Testbed::Testbed(ETestbedMode mode)
 		}},
 	};
 
-	reset_camera();
-
+	set_mode(mode);
 	set_exposure(0);
 	set_min_level(0.f);
 	set_max_level(1.f);
+
+	reset_camera();
 }
 
 Testbed::~Testbed() {
@@ -2564,6 +2686,19 @@ void Testbed::train(uint32_t batch_size) {
 		return;
 	}
 
+	if (m_testbed_mode == ETestbedMode::None) {
+		throw std::runtime_error{"Cannot train without a mode."};
+	}
+
+	// If we don't have a trainer, as can happen when having loaded training data or changed modes without having
+	// explicitly loaded a new neural network.
+	if (!m_trainer) {
+		reload_network_from_file();
+		if (!m_trainer) {
+			throw std::runtime_error{"Unable to create a neural network trainer."};
+		}
+	}
+
 	if (!m_dlss) {
 		// No immediate redraw necessary
 		reset_accumulation(false, false);
@@ -2714,9 +2849,8 @@ void Testbed::render_frame(const Matrix<float, 3, 4>& camera_matrix0, const Matr
 	Vector2f focal_length = calc_focal_length(render_buffer.in_resolution(), m_fov_axis, m_zoom);
 	Vector2f screen_center = render_screen_center();
 
-	if (m_quilting_dims != Vector2i::Ones() && m_quilting_dims != Vector2i{2, 1}) {
-		// In the case of a holoplay lenticular screen, m_scale represents the inverse distance of the head above the display.
-		m_parallax_shift.z() = 1.0f / m_scale;
+	if (!m_network) {
+		return;
 	}
 
 	switch (m_testbed_mode) {
@@ -3025,6 +3159,7 @@ void Testbed::save_snapshot(const std::string& filepath_string, bool include_opt
 
 	auto& snapshot = m_network_config["snapshot"];
 	snapshot["version"] = SNAPSHOT_FORMAT_VERSION;
+	snapshot["mode"] = to_string(m_testbed_mode);
 
 	if (m_testbed_mode == ETestbedMode::Nerf) {
 		snapshot["density_grid_size"] = NERF_GRIDSIZE();
@@ -3063,10 +3198,21 @@ void Testbed::load_snapshot(const std::string& filepath_string) {
 		throw std::runtime_error{fmt::format("File {} does not contain a snapshot.", filepath_string)};
 	}
 
+	m_network_config_path = filepath_string;
+
 	const auto& snapshot = config["snapshot"];
 
 	if (snapshot.value("version", 0) < SNAPSHOT_FORMAT_VERSION) {
-		throw std::runtime_error{"Snapshot uses an old format."};
+		throw std::runtime_error{"Snapshot uses an old format and can not be loaded."};
+	}
+
+	if (snapshot.contains("mode")) {
+		set_mode(mode_from_string(snapshot["mode"]));
+	} else if (snapshot.contains("nerf")) {
+		// To be able to load old NeRF snapshots that don't specify their mode yet
+		set_mode(ETestbedMode::Nerf);
+	} else if (m_testbed_mode == ETestbedMode::None) {
+		throw std::runtime_error{"Unknown snapshot mode. Snapshot must be regenerated with a new version of instant-ngp."};
 	}
 
 	m_aabb = snapshot.value("aabb", m_aabb);
diff --git a/src/testbed_image.cu b/src/testbed_image.cu
index 2161071..742a3b1 100644
--- a/src/testbed_image.cu
+++ b/src/testbed_image.cu
@@ -384,7 +384,7 @@ void Testbed::load_image() {
 
 void Testbed::load_exr_image() {
 	if (!m_data_path.exists()) {
-		throw std::runtime_error{m_data_path.str() + " does not exist."};
+		throw std::runtime_error{fmt::format("Image file '{}' does not exist.", m_data_path.str())};
 	}
 
 	tlog::info() << "Loading EXR image from " << m_data_path;
@@ -399,7 +399,7 @@ void Testbed::load_exr_image() {
 
 void Testbed::load_stbi_image() {
 	if (!m_data_path.exists()) {
-		throw std::runtime_error{m_data_path.str() + " does not exist."};
+		throw std::runtime_error{fmt::format("Image file '{}' does not exist.", m_data_path.str())};
 	}
 
 	tlog::info() << "Loading STBI image from " << m_data_path;
@@ -415,7 +415,7 @@ void Testbed::load_stbi_image() {
 
 void Testbed::load_binary_image() {
 	if (!m_data_path.exists()) {
-		throw std::runtime_error{m_data_path.str() + " does not exist."};
+		throw std::runtime_error{fmt::format("Image file '{}' does not exist.", m_data_path.str())};
 	}
 
 	tlog::info() << "Loading binary image from " << m_data_path;
-- 
GitLab