Browse Source

Added DebugRendererRecorder and JoltViewer, can be used to record and playback the drawn physics world (#27)

jrouwe 3 years ago
parent
commit
76912c7d90

+ 1 - 0
.gitignore

@@ -4,3 +4,4 @@
 /stats*.html
 /stats*.html
 /Docs/JoltPhysics.chm
 /Docs/JoltPhysics.chm
 /Docs/JoltPhysics.chw
 /Docs/JoltPhysics.chw
+/*.JoltRecording

+ 1 - 0
Build/CMakeLists.txt

@@ -114,4 +114,5 @@ if ("${CMAKE_SYSTEM_NAME}" STREQUAL "Windows")
 	# Windows only targets
 	# Windows only targets
 	include(${PHYSICS_REPO_ROOT}/TestFramework/TestFramework.cmake)
 	include(${PHYSICS_REPO_ROOT}/TestFramework/TestFramework.cmake)
 	include(${PHYSICS_REPO_ROOT}/Samples/Samples.cmake)
 	include(${PHYSICS_REPO_ROOT}/Samples/Samples.cmake)
+	include(${PHYSICS_REPO_ROOT}/JoltViewer/JoltViewer.cmake)
 endif()
 endif()

+ 4 - 0
Jolt/Jolt.cmake

@@ -344,6 +344,10 @@ set(JOLT_PHYSICS_SRC_FILES
 	${JOLT_PHYSICS_ROOT}/RegisterTypes.h
 	${JOLT_PHYSICS_ROOT}/RegisterTypes.h
 	${JOLT_PHYSICS_ROOT}/Renderer/DebugRenderer.cpp
 	${JOLT_PHYSICS_ROOT}/Renderer/DebugRenderer.cpp
 	${JOLT_PHYSICS_ROOT}/Renderer/DebugRenderer.h
 	${JOLT_PHYSICS_ROOT}/Renderer/DebugRenderer.h
+	${JOLT_PHYSICS_ROOT}/Renderer/DebugRendererPlayback.cpp
+	${JOLT_PHYSICS_ROOT}/Renderer/DebugRendererPlayback.h
+	${JOLT_PHYSICS_ROOT}/Renderer/DebugRendererRecorder.cpp
+	${JOLT_PHYSICS_ROOT}/Renderer/DebugRendererRecorder.h
 	${JOLT_PHYSICS_ROOT}/Skeleton/SkeletalAnimation.cpp
 	${JOLT_PHYSICS_ROOT}/Skeleton/SkeletalAnimation.cpp
 	${JOLT_PHYSICS_ROOT}/Skeleton/SkeletalAnimation.h
 	${JOLT_PHYSICS_ROOT}/Skeleton/SkeletalAnimation.h
 	${JOLT_PHYSICS_ROOT}/Skeleton/Skeleton.cpp
 	${JOLT_PHYSICS_ROOT}/Skeleton/Skeleton.cpp

+ 166 - 0
Jolt/Renderer/DebugRendererPlayback.cpp

@@ -0,0 +1,166 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <Jolt.h>
+
+#ifdef JPH_DEBUG_RENDERER
+
+#include <Renderer/DebugRendererPlayback.h>
+
+namespace JPH {
+
+void DebugRendererPlayback::Parse(StreamIn &inStream) 
+{ 
+	using ECommand = DebugRendererRecorder::ECommand;
+
+	for (;;)
+	{
+		// Read the next command
+		ECommand command;
+		inStream.Read(command);
+
+		if (inStream.IsEOF() || inStream.IsFailed())
+			return;
+
+		if (command == ECommand::CreateBatch)
+		{
+			uint32 id;
+			inStream.Read(id);
+
+			uint32 triangle_count;
+			inStream.Read(triangle_count);
+		
+			DebugRenderer::Triangle *triangles = new DebugRenderer::Triangle [triangle_count];
+			inStream.ReadBytes(triangles, triangle_count * sizeof(DebugRenderer::Triangle));
+		
+			mBatches.insert({ id, mRenderer.CreateTriangleBatch(triangles, triangle_count) });
+		
+			delete [] triangles;
+		}
+		else if (command == ECommand::CreateBatchIndexed)
+		{	
+			uint32 id;
+			inStream.Read(id);
+
+			uint32 vertex_count;
+			inStream.Read(vertex_count);
+		
+			DebugRenderer::Vertex *vertices = new DebugRenderer::Vertex [vertex_count];
+			inStream.ReadBytes(vertices, vertex_count * sizeof(DebugRenderer::Vertex));
+
+			uint32 index_count;
+			inStream.Read(index_count);
+
+			uint32 *indices = new uint32 [index_count];
+			inStream.ReadBytes(indices, index_count * sizeof(uint32));
+		
+			mBatches.insert({ id, mRenderer.CreateTriangleBatch(vertices, vertex_count, indices, index_count) });
+		
+			delete [] indices;
+			delete [] vertices;
+		}
+		else if (command == ECommand::CreateGeometry)
+		{
+			uint32 geometry_id;
+			inStream.Read(geometry_id);
+
+			AABox bounds;
+			inStream.Read(bounds.mMin);
+			inStream.Read(bounds.mMax);
+
+			DebugRenderer::GeometryRef geometry = new DebugRenderer::Geometry(bounds);
+			mGeometries[geometry_id] = geometry;
+
+			uint32 num_lods;
+			inStream.Read(num_lods);
+			for (uint32 l = 0; l < num_lods; ++l)
+			{
+				DebugRenderer::LOD lod;
+				inStream.Read(lod.mDistance);
+
+				uint32 batch_id;
+				inStream.Read(batch_id);
+				lod.mTriangleBatch = mBatches.find(batch_id)->second;
+
+				geometry->mLODs.push_back(lod);
+			}
+		}
+		else if (command == ECommand::EndFrame)
+		{
+			mFrames.push_back({});
+			Frame &frame = mFrames.back();
+
+			// Read all lines
+			uint32 num_lines = 0;
+			inStream.Read(num_lines);
+			frame.mLines.resize(num_lines);
+			for (DebugRendererRecorder::LineBlob &line : frame.mLines)
+			{
+				inStream.Read(line.mFrom);
+				inStream.Read(line.mTo);
+				inStream.Read(line.mColor);
+			}
+
+			// Read all triangles
+			uint32 num_triangles = 0;
+			inStream.Read(num_triangles);
+			frame.mTriangles.resize(num_triangles);
+			for (DebugRendererRecorder::TriangleBlob &triangle : frame.mTriangles)
+			{
+				inStream.Read(triangle.mV1);
+				inStream.Read(triangle.mV2);
+				inStream.Read(triangle.mV3);
+				inStream.Read(triangle.mColor);
+			}
+
+			// Read all texts
+			uint32 num_texts = 0;
+			inStream.Read(num_texts);
+			frame.mTexts.resize(num_texts);
+			for (DebugRendererRecorder::TextBlob &text : frame.mTexts)
+			{
+				inStream.Read(text.mPosition);
+				inStream.Read(text.mString);
+				inStream.Read(text.mColor);
+				inStream.Read(text.mHeight);
+			}
+
+			// Read all geometries
+			uint32 num_geometries = 0;
+			inStream.Read(num_geometries);
+			frame.mGeometries.resize(num_geometries);
+			for (DebugRendererRecorder::GeometryBlob &geom : frame.mGeometries)
+			{
+				inStream.Read(geom.mModelMatrix);
+				inStream.Read(geom.mModelColor);
+				inStream.Read(geom.mGeometryID);
+				inStream.Read(geom.mCullMode);
+				inStream.Read(geom.mCastShadow);
+				inStream.Read(geom.mDrawMode);
+			}
+		}
+		else
+			JPH_ASSERT(false);
+	}
+}
+
+void DebugRendererPlayback::DrawFrame(uint inFrameNumber) const
+{
+	const Frame &frame = mFrames[inFrameNumber];
+
+	for (const DebugRendererRecorder::LineBlob &line : frame.mLines)
+		mRenderer.DrawLine(line.mFrom, line.mTo, line.mColor);
+
+	for (const DebugRendererRecorder::TriangleBlob &triangle : frame.mTriangles)
+		mRenderer.DrawTriangle(triangle.mV1, triangle.mV2, triangle.mV3, triangle.mColor);
+
+	for (const DebugRendererRecorder::TextBlob &text : frame.mTexts)
+		mRenderer.DrawText3D(text.mPosition, text.mString, text.mColor, text.mHeight);
+
+	for (const DebugRendererRecorder::GeometryBlob &geom : frame.mGeometries)
+		mRenderer.DrawGeometry(geom.mModelMatrix, geom.mModelColor, mGeometries.find(geom.mGeometryID)->second, geom.mCullMode, geom.mCastShadow, geom.mDrawMode);
+}
+
+} // JPH
+
+#endif // JPH_DEBUG_RENDERER

+ 47 - 0
Jolt/Renderer/DebugRendererPlayback.h

@@ -0,0 +1,47 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#ifndef JPH_DEBUG_RENDERER
+	#error This file should only be included when JPH_DEBUG_RENDERER is defined
+#endif // !JPH_DEBUG_RENDERER
+
+#include <Renderer/DebugRendererRecorder.h>
+#include <Core/StreamIn.h>
+#include <map>
+
+namespace JPH {
+
+/// Class that can read a recorded stream from DebugRendererRecorder and plays it back trough a DebugRenderer
+class DebugRendererPlayback
+{
+public:
+	/// Constructor
+										DebugRendererPlayback(DebugRenderer &inRenderer) : mRenderer(inRenderer) { }
+
+	/// Parse a stream of frames
+	void								Parse(StreamIn &inStream);
+	
+	/// Get the number of parsed frames
+	uint								GetNumFrames() const				{ return (uint)mFrames.size(); }
+
+	/// Draw a frame
+	void								DrawFrame(uint inFrameNumber) const;
+
+private:
+	/// The debug renderer we're using to do the actual rendering
+	DebugRenderer &						mRenderer;
+
+	/// Mapping of ID to batch
+	map<uint32, DebugRenderer::Batch>	mBatches;
+
+	/// Mapping of ID to geometry
+	map<uint32, DebugRenderer::GeometryRef> mGeometries;
+
+	/// The list of parsed frames
+	using Frame = DebugRendererRecorder::Frame;
+	vector<Frame>						mFrames;
+};
+
+} // JPH

+ 156 - 0
Jolt/Renderer/DebugRendererRecorder.cpp

@@ -0,0 +1,156 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <Jolt.h>
+
+#ifdef JPH_DEBUG_RENDERER
+
+#include <Renderer/DebugRendererRecorder.h>
+
+namespace JPH {
+
+void DebugRendererRecorder::DrawLine(const Float3 &inFrom, const Float3 &inTo, ColorArg inColor) 
+{ 
+	lock_guard lock(mMutex);
+
+	mCurrentFrame.mLines.push_back({ inFrom, inTo, inColor });
+}
+
+void DebugRendererRecorder::DrawTriangle(Vec3Arg inV1, Vec3Arg inV2, Vec3Arg inV3, ColorArg inColor)
+{
+	lock_guard lock(mMutex);
+
+	mCurrentFrame.mTriangles.push_back({ inV1, inV2, inV3, inColor });
+}
+
+DebugRenderer::Batch DebugRendererRecorder::CreateTriangleBatch(const Triangle *inTriangles, int inTriangleCount)
+{
+	if (inTriangles == nullptr || inTriangleCount == 0)
+		return new BatchImpl(0);
+
+	lock_guard lock(mMutex);
+
+	mStream.Write(ECommand::CreateBatch);
+
+	uint32 batch_id = mNextBatchID++;
+	JPH_ASSERT(batch_id != 0);
+	mStream.Write(batch_id);
+	mStream.Write((uint32)inTriangleCount);
+	mStream.WriteBytes(inTriangles, inTriangleCount * sizeof(Triangle));
+
+	return new BatchImpl(batch_id);
+}
+
+DebugRenderer::Batch DebugRendererRecorder::CreateTriangleBatch(const Vertex *inVertices, int inVertexCount, const uint32 *inIndices, int inIndexCount)
+{
+	if (inVertices == nullptr || inVertexCount == 0 || inIndices == nullptr || inIndexCount == 0)
+		return new BatchImpl(0);
+
+	lock_guard lock(mMutex);
+
+	mStream.Write(ECommand::CreateBatchIndexed);
+
+	uint32 batch_id = mNextBatchID++;
+	JPH_ASSERT(batch_id != 0);
+	mStream.Write(batch_id);
+	mStream.Write((uint32)inVertexCount);
+	mStream.WriteBytes(inVertices, inVertexCount * sizeof(Vertex));
+	mStream.Write((uint32)inIndexCount);
+	mStream.WriteBytes(inIndices, inIndexCount * sizeof(uint32));
+
+	return new BatchImpl(batch_id);
+}
+
+void DebugRendererRecorder::DrawGeometry(Mat44Arg inModelMatrix, const AABox &inWorldSpaceBounds, float inLODScaleSq, ColorArg inModelColor, const GeometryRef &inGeometry, ECullMode inCullMode, ECastShadow inCastShadow, EDrawMode inDrawMode)
+{
+	lock_guard lock(mMutex);
+
+	// See if this geometry was used before
+	uint32 &geometry_id = mGeometries[inGeometry];
+	if (geometry_id == 0)
+	{
+		mStream.Write(ECommand::CreateGeometry);
+
+		// Create a new ID
+		geometry_id = mNextGeometryID++;
+		JPH_ASSERT(geometry_id != 0);
+		mStream.Write(geometry_id);
+
+		// Save bounds
+		mStream.Write(inGeometry->mBounds.mMin);
+		mStream.Write(inGeometry->mBounds.mMax);
+
+		// Save the LODs
+		mStream.Write((uint32)inGeometry->mLODs.size());
+		for (const LOD & lod : inGeometry->mLODs)
+		{
+			mStream.Write(lod.mDistance);
+			mStream.Write(static_cast<const BatchImpl *>(lod.mTriangleBatch.GetPtr())->mID);
+		}
+	}
+
+	mCurrentFrame.mGeometries.push_back({ inModelMatrix, inModelColor, geometry_id, inCullMode, inCastShadow, inDrawMode });
+}
+
+void DebugRendererRecorder::DrawText3D(Vec3Arg inPosition, const string &inString, ColorArg inColor, float inHeight)
+{ 	
+	lock_guard lock(mMutex);  
+
+	mCurrentFrame.mTexts.push_back({ inPosition, inString, inColor, inHeight });
+}
+
+void DebugRendererRecorder::EndFrame()
+{ 	
+	lock_guard lock(mMutex);  
+
+	mStream.Write(ECommand::EndFrame);
+
+	// Write all lines
+	mStream.Write((uint32)mCurrentFrame.mLines.size());
+	for (const LineBlob &line : mCurrentFrame.mLines)
+	{
+		mStream.Write(line.mFrom);
+		mStream.Write(line.mTo);
+		mStream.Write(line.mColor);
+	}
+	mCurrentFrame.mLines.clear();
+
+	// Write all triangles
+	mStream.Write((uint32)mCurrentFrame.mTriangles.size());
+	for (const TriangleBlob &triangle : mCurrentFrame.mTriangles)
+	{
+		mStream.Write(triangle.mV1);
+		mStream.Write(triangle.mV2);
+		mStream.Write(triangle.mV3);
+		mStream.Write(triangle.mColor);
+	}
+	mCurrentFrame.mTriangles.clear();
+
+	// Write all texts
+	mStream.Write((uint32)mCurrentFrame.mTexts.size());
+	for (const TextBlob &text : mCurrentFrame.mTexts)
+	{
+		mStream.Write(text.mPosition);
+		mStream.Write(text.mString);
+		mStream.Write(text.mColor);
+		mStream.Write(text.mHeight);
+	}
+	mCurrentFrame.mTexts.clear();
+
+	// Write all geometries
+	mStream.Write((uint32)mCurrentFrame.mGeometries.size());
+	for (const GeometryBlob &geom : mCurrentFrame.mGeometries)
+	{
+		mStream.Write(geom.mModelMatrix);
+		mStream.Write(geom.mModelColor);
+		mStream.Write(geom.mGeometryID);
+		mStream.Write(geom.mCullMode);
+		mStream.Write(geom.mCastShadow);
+		mStream.Write(geom.mDrawMode);
+	}
+	mCurrentFrame.mGeometries.clear();
+}
+
+} // JPH
+
+#endif // JPH_DEBUG_RENDERER

+ 121 - 0
Jolt/Renderer/DebugRendererRecorder.h

@@ -0,0 +1,121 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#ifndef JPH_DEBUG_RENDERER
+	#error This file should only be included when JPH_DEBUG_RENDERER is defined
+#endif // !JPH_DEBUG_RENDERER
+
+#include <Renderer/DebugRenderer.h>
+#include <Core/StreamOut.h>
+#include <Core/Mutex.h>
+#include <map>
+
+namespace JPH {
+
+/// Implementation of DebugRenderer that records the API invocations to be played back later
+class DebugRendererRecorder final : public DebugRenderer
+{
+public:
+	/// Constructor
+										DebugRendererRecorder(StreamOut &inStream) : mStream(inStream) { Initialize(); }
+
+	/// Implementation of DebugRenderer interface
+	virtual void						DrawLine(const Float3 &inFrom, const Float3 &inTo, ColorArg inColor) override;
+	virtual void						DrawTriangle(Vec3Arg inV1, Vec3Arg inV2, Vec3Arg inV3, ColorArg inColor) override;
+	virtual Batch						CreateTriangleBatch(const Triangle *inTriangles, int inTriangleCount) override;
+	virtual Batch						CreateTriangleBatch(const Vertex *inVertices, int inVertexCount, const uint32 *inIndices, int inIndexCount) override;
+	virtual void						DrawGeometry(Mat44Arg inModelMatrix, const AABox &inWorldSpaceBounds, float inLODScaleSq, ColorArg inModelColor, const GeometryRef &inGeometry, ECullMode inCullMode, ECastShadow inCastShadow, EDrawMode inDrawMode) override;
+	virtual void						DrawText3D(Vec3Arg inPosition, const string &inString, ColorArg inColor, float inHeight) override;
+	
+	/// Mark the end of a frame
+	void								EndFrame();
+
+	/// Control commands written into the stream
+	enum class ECommand : uint8
+	{
+		CreateBatch,
+		CreateBatchIndexed,
+		CreateGeometry,
+		EndFrame
+	};
+
+	/// Holds a single line segment
+	struct LineBlob
+	{
+		Float3							mFrom;
+		Float3							mTo;
+		Color							mColor;
+	};
+	
+	/// Holds a single triangle
+	struct TriangleBlob
+	{
+		Vec3							mV1;
+		Vec3							mV2;
+		Vec3							mV3;
+		Color							mColor;
+	};
+
+	/// Holds a single text entry
+	struct TextBlob
+	{
+		Vec3							mPosition;
+		string							mString;
+		Color							mColor;
+		float							mHeight;
+	};
+
+	/// Holds a single geometry draw call
+	struct GeometryBlob
+	{
+		Mat44							mModelMatrix;
+		Color							mModelColor;
+		uint32							mGeometryID;
+		ECullMode						mCullMode;
+		ECastShadow						mCastShadow;
+		EDrawMode						mDrawMode;
+	};
+
+	/// All information for a single frame
+	struct Frame
+	{
+		vector<LineBlob>				mLines;
+		vector<TriangleBlob>			mTriangles;
+		vector<TextBlob>				mTexts;
+		vector<GeometryBlob>			mGeometries;
+	};
+
+private:
+	/// Implementation specific batch object
+	class BatchImpl : public RefTargetVirtual
+	{
+	public:
+										BatchImpl(uint32 inID)		: mID(inID) {  }
+
+		virtual void					AddRef() override			{ ++mRefCount; }
+		virtual void					Release() override			{ if (--mRefCount == 0) delete this; }
+
+		atomic<uint32>					mRefCount = 0;
+		uint32							mID;
+	};
+
+	/// Lock that prevents concurrent access to the internal structures
+	Mutex								mMutex;
+
+	/// Stream that recorded data will be sent to
+	StreamOut &							mStream;
+
+	/// Next available ID
+	uint32								mNextBatchID = 1;
+	uint32								mNextGeometryID = 1;
+
+	/// Cached geometries and their IDs
+	map<GeometryRef, uint32>			mGeometries;
+
+	/// Data that is being accumulated for the current frame
+	Frame								mCurrentFrame;
+};
+
+} // JPH

+ 20 - 0
JoltViewer/JoltViewer.cmake

@@ -0,0 +1,20 @@
+# Root
+set(JOLT_VIEWER_ROOT ${PHYSICS_REPO_ROOT}/JoltViewer)
+
+# Source files
+set(JOLT_VIEWER_SRC_FILES
+	${JOLT_VIEWER_ROOT}/JoltViewer.cmake
+	${JOLT_VIEWER_ROOT}/JoltViewer.cpp
+	${JOLT_VIEWER_ROOT}/JoltViewer.h
+)
+
+# Group source files
+source_group(TREE ${JOLT_VIEWER_ROOT} FILES ${JOLT_VIEWER_SRC_FILES})	
+
+# Create JoltViewer executable
+add_executable(JoltViewer  ${JOLT_VIEWER_SRC_FILES})
+target_include_directories(JoltViewer PUBLIC ${JOLT_VIEWER_ROOT})
+target_link_libraries (JoltViewer LINK_PUBLIC TestFramework d3d12.lib shcore.lib)
+
+# Set the correct working directory
+set_property(TARGET JoltViewer PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${PHYSICS_REPO_ROOT}")

+ 150 - 0
JoltViewer/JoltViewer.cpp

@@ -0,0 +1,150 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#include <TestFramework.h>
+
+#include <JoltViewer.h>
+#include <Core/StreamWrapper.h>
+#include <Application/EntryPoint.h>
+#include <Renderer/DebugRendererImp.h>
+#include <UI/UIManager.h>
+#include <Application/DebugUI.h>
+#include <fstream>
+
+#ifndef JPH_DEBUG_RENDERER	
+	// Hack to still compile DebugRenderer inside the test framework when Jolt is compiled without
+	#define JPH_DEBUG_RENDERER
+	#include <Renderer/DebugRendererRecorder.cpp>
+	#include <Renderer/DebugRendererPlayback.cpp>
+	#undef JPH_DEBUG_RENDERER
+#endif
+
+JoltViewer::JoltViewer()
+{
+	// Get file name from commandline
+	string cmd_line = GetCommandLineA();
+	vector<string> args;
+	StringToVector(cmd_line, args, " ");
+	
+	// Check arguments
+	if (args.size() != 2 || args[1].empty())
+	{
+		MessageBoxA(nullptr, "Usage: JoltViewer <recording filename>", "Error", MB_OK);
+		return;
+	}
+
+	// Open file
+	ifstream stream(args[1], ifstream::in | ifstream::binary);
+	if (!stream.is_open())
+	{
+		MessageBoxA(nullptr, "Could not open recording file", "Error", MB_OK);
+		return;
+	}
+
+	// Parse the stream
+	StreamInWrapper wrapper(stream);
+	mRendererPlayback.Parse(wrapper);
+	if (mRendererPlayback.GetNumFrames() == 0)
+	{
+		MessageBoxA(nullptr, "Recording file did not contain any frames", "Error", MB_OK);
+		return;
+	}
+
+	// Draw the first frame
+	mRendererPlayback.DrawFrame(0);
+
+	// Start paused
+	Pause(true);
+
+	// Create UI
+	UIElement *main_menu = mDebugUI->CreateMenu();
+	mDebugUI->CreateTextButton(main_menu, "Help", [this](){
+		UIElement *help = mDebugUI->CreateMenu();
+		mDebugUI->CreateStaticText(help,
+			"ESC: Back to previous menu.\n"
+			"WASD + Mouse: Fly around. Hold Shift to speed up, Ctrl to slow down.\n"
+			"P: Pause / unpause simulation.\n"
+			"O: Single step simulation.\n"
+			",: Step back.\n"
+			".: Step forward.\n"
+			"Shift + ,: Play reverse.\n"
+			"Shift + .: Replay forward."
+		);
+		mDebugUI->ShowMenu(help);
+	});
+	mDebugUI->ShowMenu(main_menu);
+}
+
+bool JoltViewer::RenderFrame(float inDeltaTime)
+{
+	// If no frames were read, abort
+	if (mRendererPlayback.GetNumFrames() == 0)
+		return false;
+
+	// Handle keyboard input
+	bool shift = mKeyboard->IsKeyPressed(DIK_LSHIFT) || mKeyboard->IsKeyPressed(DIK_RSHIFT);
+	for (int key = mKeyboard->GetFirstKey(); key != 0; key = mKeyboard->GetNextKey())
+		switch (key)
+		{
+		case DIK_R:
+			// Restart
+			mCurrentFrame = 0;
+			mPlaybackMode = EPlaybackMode::Play;
+			Pause(true);
+			break;
+
+		case DIK_O:
+			// Step
+			mPlaybackMode = EPlaybackMode::Play;
+			SingleStep();
+			break;
+
+		case DIK_COMMA:
+			// Back
+			mPlaybackMode = shift? EPlaybackMode::Rewind : EPlaybackMode::StepBack;
+			Pause(false);
+			break;
+
+		case DIK_PERIOD:
+			// Forward
+			mPlaybackMode = shift? EPlaybackMode::Play : EPlaybackMode::StepForward;
+			Pause(false);
+			break;
+		}
+
+	// If paused, do nothing
+	if (inDeltaTime > 0.0f)
+	{
+		// Determine new frame number
+		switch (mPlaybackMode)
+		{
+		case EPlaybackMode::StepForward:
+			mPlaybackMode = EPlaybackMode::Stop;
+			[[fallthrough]];
+
+		case EPlaybackMode::Play:
+			if (mCurrentFrame + 1 < mRendererPlayback.GetNumFrames())
+				++mCurrentFrame;
+			break;
+
+		case EPlaybackMode::StepBack:
+			mPlaybackMode = EPlaybackMode::Stop;
+			[[fallthrough]];
+
+		case EPlaybackMode::Rewind:
+			if (mCurrentFrame > 0)
+				--mCurrentFrame;
+			break;
+
+		case EPlaybackMode::Stop:
+			break;
+		}
+
+		// Render the frame
+		mRendererPlayback.DrawFrame(mCurrentFrame);
+	}
+
+	return true;
+}
+
+ENTRY_POINT(JoltViewer)

+ 42 - 0
JoltViewer/JoltViewer.h

@@ -0,0 +1,42 @@
+// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
+// SPDX-License-Identifier: MIT
+
+#pragma once
+
+#include <Application/Application.h>
+#ifdef JPH_DEBUG_RENDERER
+	#include <Renderer/DebugRendererPlayback.h>
+#else
+	// Hack to still compile DebugRenderer inside the test framework when Jolt is compiled without
+	#define JPH_DEBUG_RENDERER
+	#include <Renderer/DebugRendererPlayback.h>
+	#undef JPH_DEBUG_RENDERER
+#endif
+
+using namespace std;
+
+// Application that views recordings produced by DebugRendererRecorder
+class JoltViewer : public Application
+{
+public:
+	// Constructor / destructor
+							JoltViewer();
+		
+	// Render the frame
+	virtual bool			RenderFrame(float inDeltaTime) override;
+
+private:
+	enum class EPlaybackMode
+	{
+		Rewind,
+		StepBack,
+		Stop,
+		StepForward,
+		Play
+	};
+
+	DebugRendererPlayback	mRendererPlayback { *mDebugRenderer };
+
+	EPlaybackMode			mPlaybackMode = EPlaybackMode::Play;						// Current playback state. Indicates if we're playing or scrubbing back/forward.
+	uint					mCurrentFrame = 0;	
+};

+ 91 - 5
PerformanceTest/PerformanceTest.cpp

@@ -12,6 +12,10 @@
 #include <Physics/PhysicsScene.h>
 #include <Physics/PhysicsScene.h>
 #include <Physics/Collision/CastResult.h>
 #include <Physics/Collision/CastResult.h>
 #include <Physics/Collision/RayCast.h>
 #include <Physics/Collision/RayCast.h>
+#ifdef JPH_DEBUG_RENDERER
+	#include <Renderer/DebugRendererRecorder.h>
+	#include <Core/StreamWrapper.h>
+#endif // JPH_DEBUG_RENDERER
 
 
 // STL includes
 // STL includes
 #include <iostream>
 #include <iostream>
@@ -79,6 +83,55 @@ const float cDeltaTime = 1.0f / 60.0f;
 // Program entry point
 // Program entry point
 int main(int argc, char** argv)
 int main(int argc, char** argv)
 {
 {
+	// Parse command line parameters
+	int specified_quality = -1;
+	int specified_threads = -1;
+	bool enable_profiler = false;
+	bool enable_debug_renderer = false;
+	for (int arg = 1; arg < argc; ++arg)
+	{
+		if (strncmp(argv[arg], "-q=", 3) == 0)
+		{
+			// Parse quality
+			if (strcmp(argv[arg] + 3, "discrete") == 0)
+			{
+				specified_quality = 0;
+			}
+			else if (strcmp(argv[arg] + 3, "linearcast") == 0)
+			{
+				specified_quality = 1;
+			}
+			else
+			{
+				cerr << "Invalid quality" << endl;
+				return 1;
+			}
+		}
+		else if (strncmp(argv[arg], "-t=", 3) == 0)
+		{
+			// Parse threads
+			specified_threads = atoi(argv[arg] + 3);
+		}
+		else if (strcmp(argv[arg], "-p") == 0)
+		{
+			enable_profiler = true;
+		}
+		else if (strcmp(argv[arg], "-r") == 0)
+		{
+			enable_debug_renderer = true;
+		}
+		else if (strcmp(argv[arg], "-h") == 0)
+		{
+			// Print usage
+			cerr << "Usage: PerformanceTest [-q=<quality>] [-t=<threads>] [-p] [-r]" << endl
+				 << "-q: Test only with specified quality (discrete, linearcast)" << endl
+				 << "-t: Test only with N threads" << endl
+				 << "-p: Write out profiles" << endl
+				 << "-r: Record debug renderer output for JoltViewer" << endl;
+			return 0;
+		}
+	}
+
 	// Register all Jolt physics types
 	// Register all Jolt physics types
 	RegisterTypes();
 	RegisterTypes();
 
 
@@ -140,11 +193,15 @@ int main(int argc, char** argv)
 	// Trace header
 	// Trace header
 	cout << "Motion Quality, Thread Count, Steps / Second, Hash" << endl;
 	cout << "Motion Quality, Thread Count, Steps / Second, Hash" << endl;
 
 
-	constexpr uint cMaxSteps = 500;
+	constexpr uint cMaxIterations = 500;
 
 
 	// Iterate motion qualities
 	// Iterate motion qualities
 	for (uint mq = 0; mq < 2; ++mq)
 	for (uint mq = 0; mq < 2; ++mq)
 	{
 	{
+		// Skip quality if another was specified
+		if (specified_quality != -1 && mq != (uint)specified_quality)
+			continue;
+
 		// Determine motion quality
 		// Determine motion quality
 		EMotionQuality motion_quality = mq == 0? EMotionQuality::Discrete : EMotionQuality::LinearCast;
 		EMotionQuality motion_quality = mq == 0? EMotionQuality::Discrete : EMotionQuality::LinearCast;
 		string motion_quality_str = mq == 0? "Discrete" : "LinearCast";
 		string motion_quality_str = mq == 0? "Discrete" : "LinearCast";
@@ -153,8 +210,16 @@ int main(int argc, char** argv)
 		for (BodyCreationSettings &body : ragdoll_settings->mParts)
 		for (BodyCreationSettings &body : ragdoll_settings->mParts)
 			body.mMotionQuality = motion_quality;
 			body.mMotionQuality = motion_quality;
 
 
+		// Determine which thread counts to test
+		vector<uint> thread_permutations;
+		if (specified_threads > 0)
+			thread_permutations.push_back((uint)specified_threads - 1);
+		else
+			for (uint num_threads = 0; num_threads < thread::hardware_concurrency(); ++num_threads)
+				thread_permutations.push_back(num_threads);
+
 		// Test thread permutations
 		// Test thread permutations
-		for (uint num_threads = 0; num_threads < thread::hardware_concurrency(); ++num_threads)
+		for (uint num_threads : thread_permutations)
 		{
 		{
 			// Create job system with desired number of threads
 			// Create job system with desired number of threads
 			JobSystemThreadPool job_system(cMaxPhysicsJobs, cMaxPhysicsBarriers, num_threads);
 			JobSystemThreadPool job_system(cMaxPhysicsJobs, cMaxPhysicsBarriers, num_threads);
@@ -206,10 +271,19 @@ int main(int argc, char** argv)
 					}
 					}
 				}
 				}
 
 
+		#ifdef JPH_DEBUG_RENDERER
+			// Open output
+			ofstream renderer_file;
+			if (enable_debug_renderer)
+				renderer_file.open(("performance_test_" + ToLower(motion_quality_str) + "_th" + ConvertToString(num_threads + 1) + ".JoltRecording").c_str(), ofstream::out | ofstream::binary | ofstream::trunc);
+			StreamOutWrapper renderer_stream(renderer_file);
+			DebugRendererRecorder renderer(renderer_stream);
+		#endif // JPH_DEBUG_RENDERER
+
 			chrono::nanoseconds total_duration(0);
 			chrono::nanoseconds total_duration(0);
 
 
 			// Step the world for a fixed amount of iterations
 			// Step the world for a fixed amount of iterations
-			for (uint iterations = 0; iterations < cMaxSteps; ++iterations)
+			for (uint iterations = 0; iterations < cMaxIterations; ++iterations)
 			{
 			{
 				JPH_PROFILE_NEXTFRAME();
 				JPH_PROFILE_NEXTFRAME();
 
 
@@ -223,8 +297,20 @@ int main(int argc, char** argv)
 				chrono::high_resolution_clock::time_point clock_end = chrono::high_resolution_clock::now();
 				chrono::high_resolution_clock::time_point clock_end = chrono::high_resolution_clock::now();
 				total_duration += chrono::duration_cast<chrono::nanoseconds>(clock_end - clock_start);
 				total_duration += chrono::duration_cast<chrono::nanoseconds>(clock_end - clock_start);
 
 
+			#ifdef JPH_DEBUG_RENDERER
+				if (enable_debug_renderer)
+				{
+					// Draw the state of the world
+					BodyManager::DrawSettings settings;
+					physics_system.DrawBodies(settings, &renderer);
+
+					// Mark end of frame
+					renderer.EndFrame();
+				}
+			#endif // JPH_DEBUG_RENDERER
+
 				// Dump profile information every 100 iterations
 				// Dump profile information every 100 iterations
-				if (iterations % 100 == 0)
+				if (enable_profiler && iterations % 100 == 0)
 				{
 				{
 					JPH_PROFILE_DUMP(ToLower(motion_quality_str) + "_th" + ConvertToString(num_threads + 1) + "_it" + ConvertToString(iterations));
 					JPH_PROFILE_DUMP(ToLower(motion_quality_str) + "_th" + ConvertToString(num_threads + 1) + "_it" + ConvertToString(iterations));
 				}
 				}
@@ -246,7 +332,7 @@ int main(int argc, char** argv)
 				ragdoll->RemoveFromPhysicsSystem();
 				ragdoll->RemoveFromPhysicsSystem();
 
 
 			// Trace stat line
 			// Trace stat line
-			cout << motion_quality_str << ", " << num_threads + 1 << ", " << double(cMaxSteps) / (1.0e-9 * total_duration.count()) << ", " << hash << endl;
+			cout << motion_quality_str << ", " << num_threads + 1 << ", " << double(cMaxIterations) / (1.0e-9 * total_duration.count()) << ", " << hash << endl;
 		}
 		}
 	}
 	}
 
 

+ 1 - 1
Samples/Samples.cmake

@@ -200,7 +200,7 @@ source_group(TREE ${SAMPLES_ROOT} FILES ${SAMPLES_SRC_FILES})
 # Create Samples executable
 # Create Samples executable
 add_executable(Samples  ${SAMPLES_SRC_FILES})
 add_executable(Samples  ${SAMPLES_SRC_FILES})
 target_include_directories(Samples PUBLIC ${SAMPLES_ROOT})
 target_include_directories(Samples PUBLIC ${SAMPLES_ROOT})
-target_link_libraries (Samples LINK_PUBLIC TestFramework d3d11.lib shcore.lib)
+target_link_libraries (Samples LINK_PUBLIC TestFramework d3d12.lib shcore.lib)
 
 
 # Set the correct working directory
 # Set the correct working directory
 set_property(TARGET Samples PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${PHYSICS_REPO_ROOT}")
 set_property(TARGET Samples PROPERTY VS_DEBUGGER_WORKING_DIRECTORY "${PHYSICS_REPO_ROOT}")

+ 6 - 4
TestFramework/Renderer/DebugRendererImp.cpp

@@ -365,8 +365,9 @@ void DebugRendererImp::DrawTriangles()
 		mShadowStateFF->Activate();
 		mShadowStateFF->Activate();
 
 
 		// Draw all primitives as seen from the light
 		// Draw all primitives as seen from the light
-		for (InstanceMap::value_type &v : mPrimitives)
-			DrawInstances(v.first, v.second.mLightStartIdx);
+		if (mNumInstances > 0)
+			for (InstanceMap::value_type &v : mPrimitives)
+				DrawInstances(v.first, v.second.mLightStartIdx);
 		for (InstanceMap::value_type &v : mTempPrimitives)
 		for (InstanceMap::value_type &v : mTempPrimitives)
 			DrawInstances(v.first, v.second.mLightStartIdx);
 			DrawInstances(v.first, v.second.mLightStartIdx);
 	}
 	}
@@ -403,8 +404,9 @@ void DebugRendererImp::DrawTriangles()
 		mTriangleStateBF->Activate();
 		mTriangleStateBF->Activate();
 
 
 		// Draw all primitives
 		// Draw all primitives
-		for (InstanceMap::value_type &v : mPrimitives)
-			DrawInstances(v.first, v.second.mGeometryStartIdx);
+		if (mNumInstances > 0)
+			for (InstanceMap::value_type &v : mPrimitives)
+				DrawInstances(v.first, v.second.mGeometryStartIdx);
 		for (InstanceMap::value_type &v : mTempPrimitives)
 		for (InstanceMap::value_type &v : mTempPrimitives)
 			DrawInstances(v.first, v.second.mGeometryStartIdx);
 			DrawInstances(v.first, v.second.mGeometryStartIdx);
 	}
 	}