//	NEBRenderer.swift
//
//	© 2026 by Jeff Weeks
//	See TermsOfUse.txt

import CompositorServices
import MetalKit	//	for MTKTextureLoader


actor NEBRenderer {

	let layerRenderer: LayerRenderer

	let playersGripHand: PlayersHand
	let playersTipHand: PlayersHand
	let computerPlayerDifficulty: Double	//	∈ 0.0 ... 1.0
	let tableHeight: Double
	let brieferPrompts: Bool
	
	var openControlPanel: @Sendable () -> Void

	let spaceGeometry: NEBSpaceGeometry
	var gameState: NEBGameState
	
	var previousPresentationTime: TimeInterval?

	let arKitSession: ARKitSession
	let worldTrackingProvider: WorldTrackingProvider
	let handTrackingProvider: HandTrackingProvider

	//	With plain unmodified headset tracking,
	//	each small Euclidean motion of the physical headset
	//	gets mapped to the corresponding small motion
	//	of the player's head in the (often non-Euclidean)
	//	virtual space. While this provides a fully
	//	authentic simulation of the non-Euclidean space,
	//	it makes it harder for the player to hold
	//	the cue stick, because the table slants
	//	upward or downward from its edge.
	//
	//	To make the game easier to play, we offer
	//	the player a "Keep table level" option.
	//	When enabled, we track only the player's
	//	orthogonal projection (a.k.a. "orthogonal shadow")
	//	onto the plane of the virtual table, instead of
	//	tracking their virtual pose in the full 3D space.
	//	We then give the player's virtual head the same
	//	rotation and height above the orthogonal shadow
	//	that the player's physical head has
	//	in the physical Euclidean space.
	//
	let keepTableLevel: Bool
	var physicalHeadsetPose: simd_double4x4?	//	in physical world coordinates
	var virtualHeadsetShadow: simd_double4x4?	//	in XZW table coordinates (Y = 0)
	var virtualHeadsetPose: simd_double4x4?		//	in full XYZW table coordinates

	var automatedCueStroke: NEBAutomatedCueStroke?	//	when the computer takes its shot
	
	//	I'm slightly uncomfortable using implicitly unwrapped optionals
	//	-- I hadn't intended to use them at all, for run-time safety --
	//	but in this case I really would rather not have to get
	//	all the Metal objects set up directly in init(),
	//	without being able to make any function calls.
	//	So let's go ahead and use implicitly unwrapped optionals here.
	
	var device: MTLDevice!
	var commandQueue: MTLCommandQueue!
	
	var billiardsTablePipelineState: MTLRenderPipelineState!
	var groundingShadowPipelineState: MTLRenderPipelineState!
	var pocketPipelineState: MTLRenderPipelineState!
	var billiardBallPipelineState: MTLRenderPipelineState!
	var cueStickPipelineState: MTLRenderPipelineState!
	var handPipelineState: MTLRenderPipelineState!
	var checkerboardTilingPipelineState: MTLRenderPipelineState!
	var messagePipelineState: MTLRenderPipelineState!

	var enabledDepthStencilState: MTLDepthStencilState!
	var groundingShadowDepthStencilState: MTLDepthStencilState!
	var pocketDepthStencilState: MTLDepthStencilState!
	var messageDepthStencilState: MTLDepthStencilState!
	
	var tableMesh: NEBMesh!
	var groundingShadowMesh: NEBMesh!
	var pocketMesh: NEBMesh!
	var ballMesh: NEBMesh!
	var cueStickMesh: NEBMesh!
	var handMeshIndices: MTLBuffer!
	
	var checkerboardPoses: MTLBuffer!
	var checkerboardCellWalls: [CellWall] = []
	var checkerboardCellWallsReducedLOD: [CellWall] = []
	
	var messageMesh: MTLBuffer!

	var ballTextures: [MTLTexture] = []
	var cueStickTexture: MTLTexture!
	var	messageTexture: MTLTexture?
	var messageOpacity: Float16?
	var messageFadeTimer: Double?
	
	var ballSampler: MTLSamplerState!
	var cueStickSampler: MTLSamplerState!
	var messageSampler: MTLSamplerState!

// MARK: -
// MARK: Init

	init?(
		layerRenderer: LayerRenderer,
		playerHandedness: PlayersHand,
		computerPlayerDifficulty: Double,	//	∈ 0.0 ... 1.0
		guardianAngelFactor: Double,		//	∈ 0.0 ... 1.0
		keepTableLevel: Bool,
		azimuthalProjection: GGVRAzimuthalProjection,
		multicolorLattice: Bool,
		geometryType: GeometryType,
		tableHeight: Double,
		brieferPrompts: Bool,
		openControlPanel: @Sendable @escaping () -> Void
	) async {

		self.layerRenderer = layerRenderer
		self.playersGripHand = playerHandedness
		self.playersTipHand  = playerHandedness.oppositeHand

		self.computerPlayerDifficulty = computerPlayerDifficulty
		self.keepTableLevel = keepTableLevel
		self.tableHeight = tableHeight
		self.brieferPrompts = brieferPrompts

		let theSpaceGeometry = NEBSpaceGeometry(geometryType: geometryType)
		self.spaceGeometry = theSpaceGeometry

		self.gameState = NEBGameState(
							spaceGeometry: theSpaceGeometry,	//	NEBGameState keeps a copy
							guardianAngelFactor: guardianAngelFactor)

		self.openControlPanel = openControlPanel
		
		self.arKitSession = ARKitSession()
		self.worldTrackingProvider =  WorldTrackingProvider()
		self.handTrackingProvider = HandTrackingProvider()

		if await !setUpMetalObjects(
					multicolorLattice: multicolorLattice,
					azimuthalProjection: azimuthalProjection) {
			return nil
		}
	}
	
	deinit {
		openControlPanel()
	}

// MARK: -
// MARK: World and hand tracking

	func startWorldAndHandTracking() async {
	
		do {
#if targetEnvironment(simulator)
			//	The simulator doesn't support hand tracking.
			try await arKitSession.run([worldTrackingProvider])
#else
			try await arKitSession.run([worldTrackingProvider, handTrackingProvider])
#endif
		} catch {
			preconditionFailure("ARKitSession error: \(error)")
		}
	}


// MARK: -
// MARK: Set up Metal objects

	func setUpMetalObjects(
		multicolorLattice: Bool,
		azimuthalProjection: GGVRAzimuthalProjection
	) async -> Bool {
		
		device = layerRenderer.device
		
		guard let theCommandQueue = device.makeCommandQueue()
		else { return false }
		commandQueue = theCommandQueue
		
		let theCheckerboardPoseMatrices = makeCheckerboardPoseMatrices()
		let theMaxTranslationInRadians = maxTranslationInRadians(theCheckerboardPoseMatrices)
		let theFogCutoffInMeters = ggvrFogCutoff(
			maxTranslationInRadians: theMaxTranslationInRadians,
			spaceGeometry: spaceGeometry,
			azimuthalProjection: azimuthalProjection)

		if  setUpPipelineStates(
				fogCutoffInMeters: theFogCutoffInMeters,
				azimuthalProjection: azimuthalProjection)
		 && setUpDepthStencilStates()
		 && setUpFixedBuffers(
		 		checkerboardPoseMatrices: theCheckerboardPoseMatrices,
		 		multicolorLattice: multicolorLattice,
		 		azimuthalProjection: azimuthalProjection)
		 && setUpTextures()
		 && setUpSamplerStates()
		{
			return true
			
		} else {
		
			assertionFailure("Unable to set up Metal resources")
			return false
		}
	}

	func setUpPipelineStates(
		fogCutoffInMeters: Double,
		azimuthalProjection: GGVRAzimuthalProjection
	) -> Bool {

		guard let theGPUFunctionLibrary = device.makeDefaultLibrary() else {
			assertionFailure("Failed to create default GPU function library")
			return false
		}
		

		//	Vertex functions

		//	Apple explicitly says it's OK
		//	to modify and reuse MTLFunctionConstantValues.
		//
		let theCompileTimeConstants = MTLFunctionConstantValues()

		func setCompileTimeConstants(
			billiardBallShading: Bool = false,
			cueStickShading: Bool = false,
			antiflickering: Bool = false,
			clearDepthBuffer: Bool = false,
			instancedRendering: Bool = false,
			fog: Bool = false
		) {

			//	setConstantValue() wants reference values.
			
			var projectionCodeAsVar: UInt8 = azimuthalProjection.gpuProjectionCode
			var geometryCodeAsVar: UInt8 = spaceGeometry.geometryType.gpuGeometryCode
			var spaceRadiusAsVar: Float32 = Float32(spaceGeometry.spaceRadius)
			var billiardBallShadingAsVar: Bool = billiardBallShading
			var cueStickShadingAsVar: Bool = cueStickShading
			var antiflickeringAsVar: Bool = antiflickering
			var clearDepthBufferAsVar: Bool = clearDepthBuffer
			var instancedRenderingAsVar: Bool = instancedRendering
			var fogAsVar: Bool = fog
			var fogCutoffInMetersAsVar = Float32(fogCutoffInMeters)
			var clipSouthernHemisphereAsVar: Bool
					= azimuthalProjection == .central
				   && spaceGeometry.geometryType == .spherical

			theCompileTimeConstants.setConstantValue(
				&projectionCodeAsVar,
				type: .uchar,
				withName: "gProjectionCode")
			theCompileTimeConstants.setConstantValue(
				&geometryCodeAsVar,
				type: .uchar,
				withName: "gGeometryCode")
			theCompileTimeConstants.setConstantValue(
				&spaceRadiusAsVar,
				type: .float,
				withName: "gSpaceRadius")
			theCompileTimeConstants.setConstantValue(
				&billiardBallShadingAsVar,
				type: .bool,
				withName: "gBilliardBallShading")
			theCompileTimeConstants.setConstantValue(
				&cueStickShadingAsVar,
				type: .bool,
				withName: "gCueStickShading")
			theCompileTimeConstants.setConstantValue(
				&antiflickeringAsVar,
				type: .bool,
				withName: "gAntiflickering")
			theCompileTimeConstants.setConstantValue(
				&clearDepthBufferAsVar,
				type: .bool,
				withName: "gClearDepthBuffer")
			theCompileTimeConstants.setConstantValue(
				&instancedRenderingAsVar,
				type: .bool,
				withName: "gInstancedRendering")
			theCompileTimeConstants.setConstantValue(
				&fogAsVar,
				type: .bool,
				withName: "gFog")
			theCompileTimeConstants.setConstantValue(
				&fogCutoffInMetersAsVar,
				type: .float,
				withName: "gFogCutoffInMeters")
			theCompileTimeConstants.setConstantValue(
				&clipSouthernHemisphereAsVar,
				type: .bool,
				withName: "gClipSouthernHemisphere")
		}

		setCompileTimeConstants()
		let theGroundingShadowVertexFunction: MTLFunction
		do {
			theGroundingShadowVertexFunction = try theGPUFunctionLibrary.makeFunction(
				name: "nonEuclideanBilliardsVertexFunction",
				constantValues: theCompileTimeConstants)
		} catch {
			assertionFailure("Failed to load theGroundingShadowVertexFunction: \(error)")
			return false
		}
		theGroundingShadowVertexFunction.label = "grounding shadow vertex function"

		setCompileTimeConstants(clearDepthBuffer: true)
		let thePocketVertexFunction: MTLFunction
		do {
			thePocketVertexFunction = try theGPUFunctionLibrary.makeFunction(
				name: "nonEuclideanBilliardsVertexFunction",
				constantValues: theCompileTimeConstants)
		} catch {
			assertionFailure("Failed to load thePocketVertexFunction: \(error)")
			return false
		}
		thePocketVertexFunction.label = "pocket vertex function"

		setCompileTimeConstants(antiflickering: true)
		let theBilliardsTableVertexFunction: MTLFunction
		do {
			theBilliardsTableVertexFunction = try theGPUFunctionLibrary.makeFunction(
				name: "nonEuclideanBilliardsVertexFunction",
				constantValues: theCompileTimeConstants)
		} catch {
			assertionFailure("Failed to load theBilliardsTableVertexFunction: \(error)")
			return false
		}
		theBilliardsTableVertexFunction.label = "billiards table vertex function"

		setCompileTimeConstants(billiardBallShading: true)
		let theBallShadingVertexFunction: MTLFunction
		do {
			theBallShadingVertexFunction = try theGPUFunctionLibrary.makeFunction(
				name: "nonEuclideanBilliardsVertexFunction",
				constantValues: theCompileTimeConstants)
		} catch {
			assertionFailure("Failed to load theBallShadingVertexFunction: \(error)")
			return false
		}
		theBallShadingVertexFunction.label = "ball-shading vertex function"

		setCompileTimeConstants(cueStickShading: true)
		let theCueStickShadingVertexFunction: MTLFunction
		do {
			theCueStickShadingVertexFunction = try theGPUFunctionLibrary.makeFunction(
				name: "nonEuclideanBilliardsVertexFunction",
				constantValues: theCompileTimeConstants)
		} catch {
			assertionFailure("Failed to load theCueStickShadingVertexFunction: \(error)")
			return false
		}
		theCueStickShadingVertexFunction.label
			= "cue stick shading vertex function"

		setCompileTimeConstants()
		let theHandVertexFunction: MTLFunction
		do {
			theHandVertexFunction = try theGPUFunctionLibrary.makeFunction(
				name: "nonEuclideanBilliardsVertexFunction",
				constantValues: theCompileTimeConstants)
		} catch {
			assertionFailure("Failed to load theHandVertexFunction: \(error)")
			return false
		}
		theHandVertexFunction.label = "hand vertex function"

		//	Only the checkerboard tiling (not the table, balls,
		//	cue stick, etc.) gets fogged. Moreover, we disable
		//	even the checkerboard tiling's fog in spherical geometry
		//	so the player can see the whole lattice clearly.
		//	In any case there's no need for fog there because,
		//	unlike in Euclidean and hyperbolic geometry,
		//	the spherical tiling has no border that needs
		//	to be hidden, nor is there any problem with
		//	distinguishing overlapping walls of the same color.
		//
		let theFogFlag = spaceGeometry.geometryType == .spherical ? false : true
		setCompileTimeConstants(
			instancedRendering: true,
			fog: theFogFlag)
		let theCheckerboardTilingVertexFunction: MTLFunction
		do {
			theCheckerboardTilingVertexFunction = try theGPUFunctionLibrary.makeFunction(
				name: "nonEuclideanBilliardsVertexFunction",
				constantValues: theCompileTimeConstants)
		} catch {
			assertionFailure("Failed to load theCheckerboardTilingVertexFunction: \(error)")
			return false
		}
		theCheckerboardTilingVertexFunction.label = "checkerboard tiling vertex function"


		//	Fragment functions
		
		guard let theBilliardsTableFragmentFunction
			= theGPUFunctionLibrary.makeFunction(name: "billiardsTableFragmentFunction")
		else {
			assertionFailure("Failed to load billiardsTableFragmentFunction")
			return false
		}
		theBilliardsTableFragmentFunction.label = "billiards table fragment function"

		guard let theGroundingShadowFragmentFunction
			= theGPUFunctionLibrary.makeFunction(name: "groundingShadowFragmentFunction")
		else {
			assertionFailure("Failed to load groundingShadowFragmentFunction")
			return false
		}
		theGroundingShadowFragmentFunction.label = "grounding shadow fragment function"

		guard let thePocketFragmentFunction
			= theGPUFunctionLibrary.makeFunction(name: "pocketFragmentFunction")
		else {
			assertionFailure("Failed to load pocketFragmentFunction")
			return false
		}
		thePocketFragmentFunction.label = "pocket fragment function"

		guard let theBilliardBallFragmentFunction
			= theGPUFunctionLibrary.makeFunction(name: "billiardBallFragmentFunction")
		else {
			assertionFailure("Failed to load billiardBallFragmentFunction")
			return false
		}
		theBilliardBallFragmentFunction.label = "billiard ball fragment function"

		guard let theCueStickFragmentFunction
			= theGPUFunctionLibrary.makeFunction(name: "cueStickFragmentFunction")
		else {
			assertionFailure("Failed to load cueStickFragmentFunction")
			return false
		}
		theCueStickFragmentFunction.label = "cue stick fragment function"

		guard let theHandFragmentFunction
			= theGPUFunctionLibrary.makeFunction(name: "handFragmentFunction")
		else {
			assertionFailure("Failed to load theHandFragmentFunction")
			return false
		}
		theHandFragmentFunction.label = "hand fragment function"

		guard let theCheckerboardTilingFragmentFunction
			= theGPUFunctionLibrary.makeFunction(name: "checkerboardTilingFragmentFunction")
		else {
			assertionFailure("Failed to load theCheckerboardTilingFragmentFunction")
			return false
		}
		theCheckerboardTilingFragmentFunction.label = "checkerboard tiling fragment function"
		
		
		//	Vertex descriptor

		//	Create a MTLVertexDescriptor describing the vertex attributes
		//	that the GPU sends along to each vertex. Each vertex attribute
		//	may be a 1-, 2-, 3- or 4-component vector.
		
		let theVertexDescriptor = MTLVertexDescriptor()
		
			//	Say where to find each attribute.

		guard let thePosOffset = MemoryLayout.offset(of: \NEBVertex.pos) else {
			assertionFailure("Failed to find offset of NEBVertex.pos")
			return false
		}
		theVertexDescriptor.attributes[VertexAttributePosition].format = MTLVertexFormat.float4
		theVertexDescriptor.attributes[VertexAttributePosition].bufferIndex = BufferIndexVertexAttributes
		theVertexDescriptor.attributes[VertexAttributePosition].offset = thePosOffset

		guard let theTexOffset = MemoryLayout.offset(of: \NEBVertex.tex) else {
			assertionFailure("Failed to find offset of NEBVertex.tex")
			return false
		}
		theVertexDescriptor.attributes[VertexAttributeTexCoords].format = MTLVertexFormat.float3
		theVertexDescriptor.attributes[VertexAttributeTexCoords].bufferIndex = BufferIndexVertexAttributes
		theVertexDescriptor.attributes[VertexAttributeTexCoords].offset = theTexOffset

			//	Say how to step through each buffer.

		theVertexDescriptor.layouts[BufferIndexVertexAttributes].stepFunction = MTLVertexStepFunction.perVertex
		theVertexDescriptor.layouts[BufferIndexVertexAttributes].stride = MemoryLayout<NEBVertex>.stride


		//	Pipeline states.

		guard let theBilliardsTablePipelineState = ggvrPipelineState(
				layerRenderer: layerRenderer,
				vertexFunction: theBilliardsTableVertexFunction,
				fragmentFunction: theBilliardsTableFragmentFunction,
				vertexDescriptor: theVertexDescriptor,
				enableBlending: false)
		else { return false }
		billiardsTablePipelineState = theBilliardsTablePipelineState
		
		guard let theGroundingShadowPipelineState = ggvrPipelineState(
				layerRenderer: layerRenderer,
				vertexFunction: theGroundingShadowVertexFunction,
				fragmentFunction: theGroundingShadowFragmentFunction,
				vertexDescriptor: theVertexDescriptor,
				enableBlending: true)
		else { return false }
		groundingShadowPipelineState = theGroundingShadowPipelineState

		guard let thePocketPipelineState = ggvrPipelineState(
				layerRenderer: layerRenderer,
				vertexFunction: thePocketVertexFunction,
				fragmentFunction: thePocketFragmentFunction,
				vertexDescriptor: theVertexDescriptor,
				enableBlending: false)
		else { return false }
		pocketPipelineState = thePocketPipelineState
		
		guard let theBilliardBallPipelineState = ggvrPipelineState(
				layerRenderer: layerRenderer,
				vertexFunction: theBallShadingVertexFunction,
				fragmentFunction: theBilliardBallFragmentFunction,
				vertexDescriptor: theVertexDescriptor,
				enableBlending: false)
		else { return false }
		billiardBallPipelineState = theBilliardBallPipelineState

		guard let theCueStickPipelineState = ggvrPipelineState(
				layerRenderer: layerRenderer,
				vertexFunction: theCueStickShadingVertexFunction,
				fragmentFunction: theCueStickFragmentFunction,
				vertexDescriptor: theVertexDescriptor,
				enableBlending: false)
		else { return false }
		cueStickPipelineState = theCueStickPipelineState
		
		guard let theHandPipelineState = ggvrPipelineState(
				layerRenderer: layerRenderer,
				vertexFunction: theHandVertexFunction,
				fragmentFunction: theHandFragmentFunction,
				vertexDescriptor: theVertexDescriptor,
				enableBlending: false)
		else { return false }
		handPipelineState = theHandPipelineState
		
		guard let theCheckerboardTilingPipelineState = ggvrPipelineState(
				layerRenderer: layerRenderer,
				vertexFunction: theCheckerboardTilingVertexFunction,
				fragmentFunction: theCheckerboardTilingFragmentFunction,
				vertexDescriptor: theVertexDescriptor,
				enableBlending: false)
		else { return false }
		checkerboardTilingPipelineState = theCheckerboardTilingPipelineState
		
		guard let theMessagePipelineState = makeMessagePipelineState(
			gpuFunctionLibrary: theGPUFunctionLibrary,
			layerRenderer: layerRenderer)
		else { return false }
		messagePipelineState = theMessagePipelineState

		return true
	}
		
	func setUpDepthStencilStates() -> Bool {

		guard let theEnabledDepthStencilState = ggvrDepthStencilState(
												device: device,
												depthCompareEnabled: true,
												depthWriteEnabled: true)
		else { return false }
		enabledDepthStencilState = theEnabledDepthStencilState

		guard let theGroundingShadowDepthStencilState = ggvrDepthStencilState(
												device: device,
												depthCompareEnabled: false,
												depthWriteEnabled: false)
		else { return false }
		groundingShadowDepthStencilState = theGroundingShadowDepthStencilState

		//	The pockets will need to clear the depth buffer beneath them,
		//	so keep depthWriteEnabled = true.
		guard let thePocketDepthStencilState = ggvrDepthStencilState(
												device: device,
												depthCompareEnabled: false,
												depthWriteEnabled: true)
		else { return false }
		pocketDepthStencilState = thePocketDepthStencilState

		guard let theMessageDepthStencilState = makeMessageDepthStencilState(device: device)
		else { return false }
		messageDepthStencilState = theMessageDepthStencilState

		return true
	}
	
	func setUpFixedBuffers(
		checkerboardPoseMatrices: [simd_double4x4],
		multicolorLattice: Bool,
		azimuthalProjection: GGVRAzimuthalProjection
	) -> Bool {

		let groundingShadowMeshRefinement: Int = 32
		let pocketMeshRefinement: Int = 64

		tableMesh = makeTableMesh(
						device: device)

		groundingShadowMesh = makeDiskMesh(
						device: device,
						r: spaceGeometry.ballShadowRadius,
						n: groundingShadowMeshRefinement)

		pocketMesh = makeDiskMesh(
						device: device,
						r: pocketRadius,
						n: pocketMeshRefinement)

		ballMesh = makeBallMesh(
						device: device)
		
		cueStickMesh = makeCueStickMesh(
						device: device)
		
		handMeshIndices = ggvrMeshFacets(
						device: device,
						vertexIndices: ggVRHandMeshIndices)

		checkerboardPoses = makeCheckerboardPoseBuffer(
						device: device,
						checkerboardPoseMatrices: checkerboardPoseMatrices)
		
		switch azimuthalProjection {

		case .central:

			//	In the central projection, all lines look straight
			//	to both eyes, so no subdivisions are needed.

			checkerboardCellWalls = makeCheckerboardCellWalls(
				device: device,
				numRecursiveSubdivisions: 0,
				withFlange: false,
				multicolorLattice: multicolorLattice)

		case .equidistant:
			
			switch spaceGeometry.geometryType {

			case .spherical:

				//	For the walls to align with the pool table
				//	we need to use exactly 5 recursive subdivisions.
				//	Fortunately the performance hit should be minimal,
				//	because the octahedral tiling of the 3-sphere
				//	contains only 16 tiles and we render only 8 of them,
				//	checkboard style.
				
				checkerboardCellWalls = makeCheckerboardCellWalls(
					device: device,
					numRecursiveSubdivisions: 5,
					withFlange: false,
					multicolorLattice: multicolorLattice)
							
			case .euclidean:

				//	In Euclidean geometry we never need to subdivide.
				
				checkerboardCellWalls = makeCheckerboardCellWalls(
					device: device,
					numRecursiveSubdivisions: 0,
					withFlange: false,
					multicolorLattice: multicolorLattice)
				
			case .hyperbolic:
								
				//	The equidistant projection of the hyperbolic tiling
				//	is the tough case.
				//
				//	For the walls to align with the pool table
				//	we need to use exactly 5 recursive subdivisions,
				//	but we'll be rendering many thousands of dodecahedra
				//	and the GPU load would be unacceptable.
				//
				//	So we use a level-of-detail ("LOD") approach instead.
				//	encodeCommands() uses the high LOD checkerboardCellWalls
				//	to draw the central dodecahedron and its 30 immediate
				//	neighbors and then draws all remaining instances
				//	using checkerboardCellWallsReducedLOD. To avoid
				//	visible gaps where a reduced LOD dodecahedron meets
				//	a high LOD one, we give each wall of the low LOD
				//	dodecahedron a small flange so that it slightly overlaps
				//	its neighbors.

				checkerboardCellWalls = makeCheckerboardCellWalls(
					device: device,
					numRecursiveSubdivisions: 5,
					withFlange: false,
					multicolorLattice: multicolorLattice)
				
				checkerboardCellWallsReducedLOD = makeCheckerboardCellWalls(
					device: device,
					numRecursiveSubdivisions: 0,
					withFlange: true,
					multicolorLattice: multicolorLattice)
			}
		}

		messageMesh = makeMessageMesh(device: device)

		return true
	}
	
	func setUpTextures() -> Bool {
	
		let theTextureLoader = MTKTextureLoader(device: device)
		let theTextureLoaderOptions: [MTKTextureLoader.Option: Any] = [
			.textureUsage:		 NSNumber(value: MTLTextureUsage.shaderRead.rawValue),
			.textureStorageMode: NSNumber(value: MTLStorageMode.`private`.rawValue)
			
			//	Mipmaps
			//
			//	Our Asset Catalog automatically generates mipmaps
			//	for all textures (at compile time? at run time? I'm not sure).
			//	To see the mipmap setting, go to NEBTextures.xcassets,
			//	open the Xcode's right-hand sidebar, and select
			//	the last tab (the Attributes inspector). Then select
			//	one of the textures, for example
			//
			//		cue stick > Universal
			//
			//	and look in the right-hand side bar to see
			//	its Mipmap Set options. Note that you have to select
			//	a specific image -- here "Universal" -- for this to work;
			//	if you'd instead just selected "cue stick" you wouldn't
			//	see the Mipmap Set options.
			//
			//	Regarding MTKTextureLoader's mipmap-related options...
			//	Apple's page
			//
			//		https://developer.apple.com/documentation/metalkit/mtktextureloader/option/1535977-allocatemipmaps
			//
			//	says that
			//
			//		[The allocateMipmaps] option applies only if the texture
			//		being loaded does not contain mipmap data.
			//		...
			//		If this key is not specified and the image being loaded
			//		contains data for mipmaps, the mipmap memory is allocated
			//		and the image data is loaded.
			//
			//	so we can omit
			//
			//		MTKTextureLoader.Option.allocateMipmaps : NSNumber(value: true),
			//
			//	Similarly, Apple's page
			//
			//		https://developer.apple.com/documentation/metalkit/mtktextureloader/1645851-newtexture
			//
			//	says that
			//
			//		"When using [newTexture()], the texture loader
			//		ignores the generateMipmaps … option."
			//
			//	Instead, the Asset Catalog provides the mipmaps.
			//	So we can omit
			//
			//		MTKTextureLoader.Option.generateMipmaps : NSNumber(value: true)
			//
		]
		
		//	Cube map coordinate conventions
		//
		//	Apple's MTLTexture documentation says which index (in 0...5)
		//	maps to which face of the cube, but doesn't say how it maps
		//	to that face. In practice the layout seems to be
		//
		//
		//		       +------+                   +---→ s
		//		       |      |                   |
		//		       |  y+  |                   ↓ t
		//		       |      |
		//		+------+------+------+------+
		//		|      |      |      |      |
		//		|  x-  |  z+  |  x+  |  z-  |
		//		|      |      |      |      |
		//		+------+------+------+------+
		//		       |      |
		//		       |  y-  |
		//		       |      |
		//		       +------+
		//
		//
		//	with each face oriented the same as in the old
		//	OpenGL ARB_texture_cube_map extension, namely
		//
		//				   largest
		//		  index     coord   pre-s  pre-t
		//		  -----    -------  -----  -----
		//			0       x > 0    -z     -y
		//			1       x < 0    +z     -y
		//			2       y > 0    +x     +z
		//			3       y < 0    +x     -z
		//			4       z > 0    +x     -y
		//			5       z < 0    -x     -y
		//
		//	with the final s and t texture coordinates given by
		//
		//		s = ( ( pre-s / largest-coord ) + 1 ) / 2
		//		t = ( ( pre-t / largest-coord ) + 1 ) / 2
		//
		//	These conventions agree with the diagram shown above.
		//
		//	Note:  An Xcode Asset Catalog's images seem to be
		//	upside down relative to the conventions just described.
		//	Most likely this is just a question of whether
		//	the image rows are being read top-down or bottom-up.
		//
		ballTextures = []
		do {
			for i in 0 ..< numBalls {
				let theBallTexture = try theTextureLoader.newTexture(
										name: "\(i) ball",
										scaleFactor: 1.0,
										bundle: nil,
										options: theTextureLoaderOptions)
				ballTextures.append(theBallTexture)
			}
		} catch {
			assertionFailure("Failed to load one or more ball textures. Error:  \(error)")
			return false
		}
	
		do {
			cueStickTexture = try theTextureLoader.newTexture(
									name: "cue stick",
									scaleFactor: 1.0,
									bundle: nil,
									options: theTextureLoaderOptions)
		} catch {
			assertionFailure("Failed to load cue stick textures. Error:  \(error)")
			return false
		}
		
		//	updateGameMessageState() triggers the creation of the messageTexture.
		messageTexture = nil
		
		return true
	}
	
	func setUpSamplerStates() -> Bool {

		//	Presumably the MTLSamplerAddressModes are irrelevant with a cube map?
		guard let theBallSampler = ggvrSamplerState(
									device: device,
									sMode: .repeat,
									tMode: .repeat)
		else {
			assertionFailure("Couldn't create theBallSampler")
			return false
		}
		ballSampler = theBallSampler
		
		//	tMode:.repeat is essential to accommodate meridional phase shift.
		guard let theCueStickSampler = ggvrSamplerState(
										device: device,
										sMode: .clampToEdge,
										tMode: .repeat)
		else {
			assertionFailure("Couldn't create theCueStickSampler")
			return false
		}
		cueStickSampler = theCueStickSampler
		
		guard let theMessageSampler = makeMessageSampler(device: device)
		else {
			assertionFailure("Couldn't create theMessageSampler")
			return false
		}
		messageSampler = theMessageSampler
		
		return true
	}


// MARK: -
// MARK: Render loop

	func runRenderLoop() {

		var keepRendering = true	//	remains true until the immersive space gets closed

		while keepRendering {
		
			switch layerRenderer.state {
			
			case .paused:
				layerRenderer.waitUntilRunning()
				
			case .running:
				autoreleasepool {
					renderOneFrame()
				}
				
			case .invalidated:
				fallthrough
			@unknown default:
				keepRendering = false
			}
		}
		
		arKitSession.stop()
	}

	func renderOneFrame() {
	
		guard let theFrame = layerRenderer.queryNextFrame(),
			  let theTiming = theFrame.predictTiming()
		else {
			assertionFailure("Couldn't get theFrame and/or theTiming")
			return
		}
		

		//----------
		//	Update
		//----------

		//	Update anything that doesn't depend on the headset position.
		//	The startUpdate() / endUpdate() calls help
		//	Compositor Services plan its timing for future frames.

		theFrame.startUpdate()
		
		let thePredictedPresentationTime = LayerRenderer.Clock.Instant.epoch.duration(
											to: theTiming.presentationTime).timeInterval
		let theFrameInterval = ggVRComputeFrameInterval(
								predictedPresentationTime: thePredictedPresentationTime,
								previousPresentationTime: previousPresentationTime)
		previousPresentationTime = thePredictedPresentationTime

		let theActivePlayerTookAShot	//	Did active player just now hit the cue ball?
			= gameState.maybeTakeShot(
				cueStick: gameState.cueStick[gameState.whoseTurn]!,
				handTrackingInterval: theFrameInterval)

		computerMaybeBeginsStroke(	//	Computer begins stroke, if appropriate
			predictedPresentationTime: thePredictedPresentationTime)

		_ = gameState.update(for: theFrameInterval)
		
		theFrame.endUpdate()


		//-----------------
		//	Get drawables
		//-----------------

		//	Wait until the optimal time for querying
		//	the headset and hand positions.
		LayerRenderer.Clock().wait(until: theTiming.optimalInputTime)

		//	Normally we get only one drawable, with target=.builtIn.
		//	But when capturing high-quality video, we'll get two drawables,
		//	with targets .builtIn and .capture.
		let theDrawables = theFrame.queryDrawables()
		if theDrawables.isEmpty {
			//	When dismissing the immersive space, we may fail to get a drawable
			//	on our last pass through the render loop. This is OK.
			return
		}
		

		//----------------
		//	Submit frame
		//----------------

		//	The startSubmission() / endSubmission() calls help
		//	Compositor Services plan its timing for future frames.

		theFrame.startSubmission()
		
		let theVirtualHands: [PlayersHand: VirtualHand]
		if let theBuiltInDrawable = theDrawables.first(where: {drawable in
													drawable.target == .builtIn }
		) {

			let thePresentationTime = LayerRenderer.Clock.Instant.epoch.duration(to:
				theBuiltInDrawable.frameTiming.presentationTime).timeInterval

			//	The device anchor is nil for the first frame or so,
			//	and possibly again later on if the Vision Pro's cameras
			//	get blocked or the room gets too dark.
			if let theDeviceAnchor = worldTrackingProvider.queryDeviceAnchor(
										atTimestamp: thePresentationTime)
			{

				let theNewPhysicalHeadsetPose = convertFloat4x4toDouble4x4(
					theDeviceAnchor.originFromAnchorTransform)

				let theVirtualHeadsetPose = updateVirtualHeadsetPose(
					newPhysicalHeadsetPose: theNewPhysicalHeadsetPose)
					
				theVirtualHands = ggvrVirtualHands(
									physicalHeadsetPose: theNewPhysicalHeadsetPose,
									virtualHeadsetPose: theVirtualHeadsetPose,
									handTrackingProvider: handTrackingProvider,
									spaceGeometry: spaceGeometry)
							
				updateMessage(	//	may affect cue stick visibility
					virtualGripHand: theVirtualHands[playersGripHand],
					activePlayerTookAShot: theActivePlayerTookAShot,
					frameInterval: theFrameInterval)

				updateCueStick(
					virtualHands: theVirtualHands,
					predictedPresentationTime: thePredictedPresentationTime)
				
				if gameState.balls[0].status == .inHand {
					updateInHandCueBall(virtualHands: theVirtualHands)
				}

			} else {	//	DeviceAnchor is unavailable

				physicalHeadsetPose = nil
				//	Leave the virtualHeadsetPose where it is.
				theVirtualHands = [:]
			}
										
		} else {
			theVirtualHands = [:]
		}
		
		for theDrawable in theDrawables {	//	target ∈ { .builtIn, .capture }
		
			renderTheFrame(
				drawable: theDrawable,
				virtualHands: theVirtualHands)
		}
		
		theFrame.endSubmission()
	}
		
	func computerMaybeBeginsStroke(
		predictedPresentationTime: TimeInterval
	) {
	
		//	Is it time for the computer opponent to initiate a shot?

		if gameState.whoseTurn == .computer {

			if !gameState.ballsAreRolling()
			&& !shotIsInProgress()
			&& !gameState.gameIsOver {
		
				//	If the computer player has the cue ball inHand,
				//	place it on the table.
				if gameState.balls[0].status == .inHand {
					gameState.inHandCueBallPose = nil
					gameState.balls[0].placement = validCueBallPlacementNearCenter()
					gameState.balls[0].status = .inPlay
					Task { await NEBSoundPlayer.shared.play(.ballOnTable, volume: 1.0) }
				}

				//	Let the computer player initiate the shot.
				planShot(
					difficulty: computerPlayerDifficulty,
					startTime: predictedPresentationTime)
			}
		}
	}

	func updateVirtualHeadsetPose(
		newPhysicalHeadsetPose: simd_double4x4
	) -> simd_double4x4 {
			
		let thePreviousPhysicalHeadsetPose = physicalHeadsetPose ?? newPhysicalHeadsetPose
		physicalHeadsetPose = newPhysicalHeadsetPose	// for next time

		func virtualPoseAboveShadow(
			physicalPose: simd_double4x4
		) -> simd_double4x4 {	//	returns virtual pose
		
			var thePhysicalPoseAboveShadow = physicalPose
			thePhysicalPoseAboveShadow[3][0] = 0.0
			thePhysicalPoseAboveShadow[3][1] -= tableHeight
			thePhysicalPoseAboveShadow[3][2] = 0.0
			
			let theVirtualPoseAboveShadow = virtualPose(
				correspondingToPhysicalPose: thePhysicalPoseAboveShadow,
				spaceGeometry: spaceGeometry)
								
			return theVirtualPoseAboveShadow
		}

		let theNewVirtualHeadsetPose: simd_double4x4
		if keepTableLevel {
		
			let theShadowIncrementInMeters
				= newPhysicalHeadsetPose[3]
				- thePreviousPhysicalHeadsetPose[3]

			let theShadowIncrementInVirtualRadians
				= theShadowIncrementInMeters / spaceGeometry.spaceRadius

			let theVirtualShadowIncrement
				= geodesicTranslation(
					v: SIMD3<Double>(
						theShadowIncrementInVirtualRadians[0],
						0.0,	//	shadow ignores y coordinate
						theShadowIncrementInVirtualRadians[2]
					),
					geometryType: spaceGeometry.geometryType)
			
			let thePreviousShadowPose
				= virtualHeadsetShadow ?? matrix_identity_double4x4
			
			let theIncrementedShadowPose	//	right-to-left matrix action
				= thePreviousShadowPose
				* theVirtualShadowIncrement

			let theNewShadowPose = applyGramSchmidt(
				to: theIncrementedShadowPose,
				geometryType: spaceGeometry.geometryType)

			let theVirtualPoseAboveShadow = virtualPoseAboveShadow(
				physicalPose: newPhysicalHeadsetPose)
			
			theNewVirtualHeadsetPose	//	right-to-left matrix action
				= theNewShadowPose
				* theVirtualPoseAboveShadow
			
			virtualHeadsetShadow = theNewShadowPose
			virtualHeadsetPose = theNewVirtualHeadsetPose
		
		} else {	//	!keepTableLevel
		
			let thePhysicalIncrement	//	right-to-left matrix action
				= thePreviousPhysicalHeadsetPose.inverse
				* newPhysicalHeadsetPose
			
			let theVirtualIncrement = virtualPose(
				correspondingToPhysicalPose: thePhysicalIncrement,
				spaceGeometry: spaceGeometry)

			let thePreviousVirtualHeadsetPose
				= virtualHeadsetPose ??
					virtualPoseAboveShadow(
						physicalPose: thePreviousPhysicalHeadsetPose)

			let theIncrementedPose
				= thePreviousVirtualHeadsetPose	//	right-to-left matrix action
				* theVirtualIncrement

			theNewVirtualHeadsetPose = applyGramSchmidt(
				to: theIncrementedPose,
				geometryType: spaceGeometry.geometryType)

			virtualHeadsetPose = theNewVirtualHeadsetPose
		}
		
		return theNewVirtualHeadsetPose
	}

	func updateMessage(
		virtualGripHand: VirtualHand?,
		activePlayerTookAShot: Bool,
		frameInterval: Double
	) {

		guard let thePhysicalHeadsetPoseInWorldCoords = physicalHeadsetPose
		else { return }

		if gameState.updateGameMessageState(
			physicalHeadsetPoseInWorldCoordinates: thePhysicalHeadsetPoseInWorldCoords,
			virtualGripHand: virtualGripHand,
			activePlayerTookShot: activePlayerTookAShot) {
		
			//	The gameMessageState has changed.
			//	Update the messageTexture accordingly.
			
			let theMessageText = gameState.gameMessageState.messageText(
				hand: playersGripHand,
				geometryType: spaceGeometry.geometryType,
				howGameEnded: gameState.howGameEnded)

			if !theMessageText.isEmpty {

				messageTexture = makeMessageTexture(
									device: device,
									commandQueue: commandQueue,
									inputText: theMessageText )
				messageOpacity = 1.0
				messageFadeTimer = brieferPrompts ? 1.0 : nil
									
			} else {
			
				//	theMessageText is now empty. But rather than
				//	making the previous messageTexture disappear abruptly,
				//	let's leave it in place and fade it way gradually.
				
				messageFadeTimer = messageFadeDuration
			}
		}
		
		if var theMessageFadeTimer = messageFadeTimer {
		
			theMessageFadeTimer -= frameInterval
			if theMessageFadeTimer <= 0.0 {
			
				messageTexture = nil
				messageOpacity = nil
				messageFadeTimer = nil
				
			} else {
			
				//	Because human perceive brightness non-linearly,
				//	a linear fade seems too abrupt at the end.
				//	So let's try a quadratic fade instead.
				let theFractionalTime = min(1.0, theMessageFadeTimer / messageFadeDuration)
				messageOpacity = Float16(theFractionalTime * theFractionalTime)
				messageFadeTimer = theMessageFadeTimer
			}
		}
	}

	func updateCueStick(
		virtualHands: [PlayersHand: VirtualHand],
		predictedPresentationTime: TimeInterval
	) {
	
		//	Human's cue stick

		//	Pick the two points we'd like human's cue stick to pass through,
		//	in virtual table coordinates.
		let theGripPoint   = virtualHands[playersGripHand]?.cueStickGripPoint
		let theBridgePoint = virtualHands[playersTipHand]?.cueStickBridgePoint

		let theHumansCueStickPose = gameState.gameMessageState.cueStickIsAvailable ?
			getVirtualCueStickPose(
				gripPoint: theGripPoint,
				bridgePoint: theBridgePoint,
				cueBallIsInHand: gameState.balls[0].status == .inHand) :
			nil
		gameState.cueStick[.human]!.movedTo(theHumansCueStickPose)


		//	Computer's cue stick
		
		let theComputersCueStickPose: simd_double4x4?
		switch gameState.whoseTurn {
		case .human:
			theComputersCueStickPose = nil
		case .computer:
			theComputersCueStickPose = cueStickPose(atTime: predictedPresentationTime)
		}
		gameState.cueStick[.computer]!.movedTo(theComputersCueStickPose)
	}

	func updateInHandCueBall(
		virtualHands: [PlayersHand: VirtualHand]
	) {

		if gameState.balls[0].status != .inHand {
			assertionFailure("updateInHandCueBall() was called when cue ball was not inHand")
			return
		}
		
		switch gameState.whoseTurn {
		
		case .human:

			let theInHandPoint = virtualHands[playersGripHand]?.cueBallInHandPoint
		
			gameState.inHandCueBallPose
				= gameState.gameMessageState.cueBallIsAvailable ?
					getVirtualInHandCueBallPose(cueBallCenterPoint: theInHandPoint) :
					nil
			
			gameState.maybePlaceCueBall()
		
		case .computer:
		
			let theHeightAboveTable = 0.35	//	in meters

			gameState.inHandCueBallPose = axisAlignedTranslation(
				axis: 1,
				distanceInRadians: theHeightAboveTable / spaceGeometry.spaceRadius,
				geometryType: spaceGeometry.geometryType)
		}
	}

	func renderTheFrame(
		drawable: LayerRenderer.Drawable,
		virtualHands: [PlayersHand: VirtualHand]
	) {

		let thePresentationTime = LayerRenderer.Clock.Instant.epoch.duration(to:
			drawable.frameTiming.presentationTime).timeInterval

		drawable.deviceAnchor = worldTrackingProvider.queryDeviceAnchor(
									atTimestamp: thePresentationTime)

		guard let theCommandBuffer = commandQueue.makeCommandBuffer() else {
			//	If we call startSubmission() and then don't render a frame,
			//	the app crashes. So if we can't create theCommandBuffer
			//	we might as well exit now.
			preconditionFailure("Failed to make theCommandBuffer")
		}

		let theRenderPassDescriptor = ggvrRenderPassDescriptor(
										layerRenderer: layerRenderer,
										drawable: drawable)

		let theViewProjection = ggvrMakeViewProjection(
			virtualHeadsetPose: virtualHeadsetPose ?? matrix_identity_double4x4,
			drawable: drawable,
			spaceGeometry: spaceGeometry)

		let theMessageUniforms = makeMessageUniforms(
									drawable: drawable,
									messageTexture: messageTexture,
									verticalOffset: 0.0) // panel's lower edge at eye level

		if let theRenderEncoder = theCommandBuffer.makeRenderCommandEncoder(
										descriptor: theRenderPassDescriptor) {
			theRenderEncoder.label = "Non-Euclidean Billiards render encoder"

			ggvrEncodeViewports(
				drawable: drawable,
				renderEncoder: theRenderEncoder)

			encodeCommands(
				renderEncoder: theRenderEncoder,
				viewProjection: theViewProjection,
				virtualHands: virtualHands,
				messageUniforms: theMessageUniforms)
				
			theRenderEncoder.endEncoding()
		}
		
		drawable.encodePresent(commandBuffer: theCommandBuffer)
		theCommandBuffer.commit()
	}
	
	func encodeCommands(
		renderEncoder: MTLRenderCommandEncoder,
		viewProjection: GGVRViewProjection,
		virtualHands: [PlayersHand: VirtualHand],
		messageUniforms: MessageUniforms?
	) {
	
		renderEncoder.pushDebugGroup("Draw the whole scene")

		renderEncoder.setCullMode(.back)			//	our default
		renderEncoder.setFrontFacing(.clockwise)	//	Metal's default (and ours)

		var theViewProjection = viewProjection	//	setVertexBytes requires a 'var'
		renderEncoder.setVertexBytes(
						&theViewProjection,
						length: MemoryLayout.size(ofValue: theViewProjection),
						index: BufferIndexViewProjection)
		
		//	Table
		//
		//	The table must be rendered first,
		//	so the grounding shadows and the pockets
		//	can be rendered on top of it.

		renderEncoder.setRenderPipelineState(billiardsTablePipelineState)
		renderEncoder.setDepthStencilState(enabledDepthStencilState)
		renderEncoder.setVertexBuffer(
						tableMesh.vertexBuffer,
						offset: 0,
						index: BufferIndexVertexAttributes)

		var theTablePose = matrix_identity_float4x4	//	table pose in table coordinates
		renderEncoder.setVertexBytes(
			&theTablePose,
			length: MemoryLayout.size(ofValue: theTablePose),
			index: BufferIndexModelMatrix)

		renderEncoder.drawIndexedPrimitives(
						type: .triangle,
						indexCount: tableMesh.indexCount,
						indexType: .uint16,
						indexBuffer: tableMesh.indexBuffer,
						indexBufferOffset: 0,
						instanceCount: 1)
		
		//	Grounding shadows
		//
		//	The grounding shadows must get rendered directly on top of the table.
		//	The groundingShadowDepthStencilState neither reads from nor writes to
		//	the depth buffer.
		
		renderEncoder.setRenderPipelineState(groundingShadowPipelineState)
		renderEncoder.setDepthStencilState(groundingShadowDepthStencilState)
		renderEncoder.setVertexBuffer(
						groundingShadowMesh.vertexBuffer,
						offset: 0,
						index: BufferIndexVertexAttributes)

		for i in 0 ..< numBalls {
		
			let theBall = gameState.balls[i]

			if theBall.status == .inPlay {
		
				var theShadowPose = convertDouble4x4toFloat4x4(theBall.placement)
				renderEncoder.setVertexBytes(
					&theShadowPose,
					length: MemoryLayout.size(ofValue: theShadowPose),
					index: BufferIndexModelMatrix)

				renderEncoder.drawIndexedPrimitives(
								type: .triangle,
								indexCount: groundingShadowMesh.indexCount,
								indexType: .uint16,
								indexBuffer: groundingShadowMesh.indexBuffer,
								indexBufferOffset: 0,
								instanceCount: 1)
			}
		}
		
		//	Pockets
		//
		//	The pockets must get rendered immediately
		//	after the table and the grounding shadows.
		//	The pocketPipelineState enables clearDepthBuffer,
		//	so in effect it erases any and all preexisting
		//	content where we're drawing the pockets.
		
		renderEncoder.setRenderPipelineState(pocketPipelineState)
		renderEncoder.setDepthStencilState(pocketDepthStencilState)
		renderEncoder.setVertexBuffer(
						pocketMesh.vertexBuffer,
						offset: 0,
						index: BufferIndexVertexAttributes)

		for thePose in spaceGeometry.pocketPoses {
		
			var thePocketPose = convertDouble4x4toFloat4x4(thePose)
			renderEncoder.setVertexBytes(
				&thePocketPose,
				length: MemoryLayout.size(ofValue: thePocketPose),
				index: BufferIndexModelMatrix)

			renderEncoder.drawIndexedPrimitives(
							type: .triangle,
							indexCount: pocketMesh.indexCount,
							indexType: .uint16,
							indexBuffer: pocketMesh.indexBuffer,
							indexBufferOffset: 0,
							instanceCount: 1)
		}

		//	Now that we've drawn the table, grounding shadows and pockets
		//	(which use the depth buffer in unusual ways),
		//	we can draw all the "normal" content.

		//	Balls
		
		let theWorldUpVector = SIMD4<Double>(0.0, 1.0, 0.0, 0.0)

		renderEncoder.setRenderPipelineState(billiardBallPipelineState)
		renderEncoder.setDepthStencilState(enabledDepthStencilState)
		renderEncoder.setVertexBuffer(
						ballMesh.vertexBuffer,
						offset: 0,
						index: BufferIndexVertexAttributes)
		renderEncoder.setFragmentSamplerState(
						ballSampler,
						index: SamplerIndexPrimary)
			
		for i in 0 ..< numBalls {

			let theBall = gameState.balls[i]

			if theBall.status == .inPlay {
			
				var theBallPose = convertDouble4x4toFloat4x4(
									theBall.placement	//	right-to-left matrix action
								  * spaceGeometry.ballLift
								  * theBall.rotation)
				renderEncoder.setVertexBytes(
					&theBallPose,
					length: MemoryLayout.size(ofValue: theBallPose),
					index: BufferIndexModelMatrix)
				
				let theUpVector4 = geometricInverse(
									of: theBall.rotation,
									geometryType: spaceGeometry.geometryType)
								 * theWorldUpVector
				var theUpVector3 = SIMD3<Float16>(
										Float16(theUpVector4.x),
										Float16(theUpVector4.y),
										Float16(theUpVector4.z))
				renderEncoder.setVertexBytes(
					&theUpVector3,
					length: MemoryLayout.size(ofValue: theUpVector3),
					index: BufferIndexUpVector)

				var theColorSaturation = Float16(
					makeScreenshots ?
						1.0 :						//	Color all balls
						theBall.colorSaturation)	//	Color active player's balls only
				renderEncoder.setFragmentBytes(
					&theColorSaturation,
					length: MemoryLayout.size(ofValue: theColorSaturation),
					index: BufferIndexColoration)
				
				renderEncoder.setFragmentTexture(
								ballTextures[i],
								index: TextureIndexPrimary)

				renderEncoder.drawIndexedPrimitives(
								type: .triangle,
								indexCount: ballMesh.indexCount,
								indexType: .uint16,
								indexBuffer: ballMesh.indexBuffer,
								indexBufferOffset: 0,
								instanceCount: 1)
			}
		}

		if let theInHandCueBallPose = gameState.inHandCueBallPose {	//	in table coordinates
		
			var theInHandPose = convertDouble4x4toFloat4x4(theInHandCueBallPose)
			renderEncoder.setVertexBytes(
				&theInHandPose,
				length: MemoryLayout.size(ofValue: theInHandPose),
				index: BufferIndexModelMatrix)

			//	The inHandCueBallPose is a plain translation, so its
			//	local up-vector is a reasonable global up-vector.
			var theUpVector = SIMD3<Float16>(0.0, 1.0, 0.0)
			renderEncoder.setVertexBytes(
				&theUpVector,
				length: MemoryLayout.size(ofValue: theUpVector),
				index: BufferIndexUpVector)

			var theColorSaturation = Float16(1.0)
			renderEncoder.setFragmentBytes(
				&theColorSaturation,
				length: MemoryLayout.size(ofValue: theColorSaturation),
				index: BufferIndexColoration)

			renderEncoder.setFragmentTexture(
							ballTextures[0],
							index: TextureIndexPrimary)

			renderEncoder.drawIndexedPrimitives(
							type: .triangle,
							indexCount: ballMesh.indexCount,
							indexType: .uint16,
							indexBuffer: ballMesh.indexBuffer,
							indexBufferOffset: 0,
							instanceCount: 1)
		}
		
		//	Cue sticks
		
		renderEncoder.setRenderPipelineState(cueStickPipelineState)
		renderEncoder.setDepthStencilState(enabledDepthStencilState)
		renderEncoder.setVertexBuffer(
						cueStickMesh.vertexBuffer,
						offset: 0,
						index: BufferIndexVertexAttributes)
		renderEncoder.setFragmentTexture(
						cueStickTexture,
						index: TextureIndexPrimary)
		renderEncoder.setFragmentSamplerState(
						cueStickSampler,
						index: SamplerIndexPrimary)

		func renderCueStick(
			_ cueStick: CueStick
		) {
			if let theCueStickPose = cueStick.current,
			   cueStick.opacity > 0.0 {

				var theCueStickPoseFloat = convertDouble4x4toFloat4x4(theCueStickPose)
				renderEncoder.setVertexBytes(
					&theCueStickPoseFloat,
					length: MemoryLayout.size(ofValue: theCueStickPoseFloat),
					index: BufferIndexModelMatrix)

				var theOpacity = Float16(cueStick.opacity)
				renderEncoder.setFragmentBytes(
					&theOpacity,
					length: MemoryLayout.size(ofValue: theOpacity),
					index: BufferIndexColoration)

				renderEncoder.drawIndexedPrimitives(
								type: .triangle,
								indexCount: cueStickMesh.indexCount,
								indexType: .uint16,
								indexBuffer: cueStickMesh.indexBuffer,
								indexBufferOffset: 0,
								instanceCount: 1)
			}
		}
		renderCueStick(gameState.cueStick[.human]!)
		renderCueStick(gameState.cueStick[.computer]!)
	
		//	Hands
			
		//	To keep the triangle count low, the player's hands
		//	are 2D surfaces, not 3D models.

		if makeScreenshots {
		
			//	Even though the hands look 3D in the app itself,
			//	in a 2D screenshot they look like beige blogs.
			//	So let's omit the hands when making screenshots.
			
		} else {	//	regular rendering

			renderEncoder.setRenderPipelineState(handPipelineState)
			renderEncoder.setDepthStencilState(enabledDepthStencilState)

			for thePlayersHand in PlayersHand.allCases {

				guard let theVirtualHands = virtualHands[thePlayersHand]
				else { continue }

				var theVirtualHandPose = convertDouble4x4toFloat4x4(theVirtualHands.pose)
				renderEncoder.setVertexBytes(
					&theVirtualHandPose,
					length: MemoryLayout.size(ofValue: theVirtualHandPose),
					index: BufferIndexModelMatrix)

				let theHandVertices = theVirtualHands.meshVertices.map()
				{ position in
					NEBVertex(pos: position, tex: .zero)
				}
				renderEncoder.setVertexBytes(
					theHandVertices,
					length: theHandVertices.count * MemoryLayout<NEBVertex>.stride,
					index: BufferIndexVertexAttributes)
				
				//	Render back of hand
				
				//	The left and right hand meshes are topologically
				//	mirror images of each other.
				switch thePlayersHand {
				case .left:  renderEncoder.setCullMode(.front)
				case .right: renderEncoder.setCullMode(.back)
				}

				//	A selection of reasonable skin tones
				//	in linear Display P3:
				//
				//		SIMD3<Float16>(0.2351, 0.0966, 0.0271)
				//		SIMD3<Float16>(0.5067, 0.2492, 0.0765)
				//		SIMD3<Float16>(0.6863, 0.4235, 0.1712)
				//		SIMD3<Float16>(0.8192, 0.5507, 0.2408)
				//		SIMD3<Float16>(0.9482, 0.7180, 0.4439)
				//
				var theBackColor = SIMD4<Float16>(0.59, 0.33, 0.12, 1.0)
				renderEncoder.setFragmentBytes(
					&theBackColor,
					length: MemoryLayout.size(ofValue: theBackColor),
					index: BufferIndexColoration)

				renderEncoder.drawIndexedPrimitives(
								type: .triangle,
								indexCount: ggVRHandMeshIndices.count,
								indexType: .uint16,
								indexBuffer: handMeshIndices,
								indexBufferOffset: 0)
				
				//	Render palm of hand
				
				//	Draw the faces that got culled when drawing
				//	the back of the hand (above).
				switch thePlayersHand {
				case .left:  renderEncoder.setCullMode(.back)
				case .right: renderEncoder.setCullMode(.front)
				}

				//	See skin tones above.
				var thePalmColor = SIMD4<Float16>(0.82, 0.55, 0.24, 1.0)
				renderEncoder.setFragmentBytes(
					&thePalmColor,
					length: MemoryLayout.size(ofValue: thePalmColor),
					index: BufferIndexColoration)

				renderEncoder.drawIndexedPrimitives(
								type: .triangle,
								indexCount: ggVRHandMeshIndices.count,
								indexType: .uint16,
								indexBuffer: handMeshIndices,
								indexBufferOffset: 0)
			}
			renderEncoder.setCullMode(.back)	//	restore our default
		}

		//	Checkerboard tiling

		renderEncoder.setRenderPipelineState(checkerboardTilingPipelineState)
		renderEncoder.setDepthStencilState(enabledDepthStencilState)
		renderEncoder.setCullMode(.none)

		//	The tiling matrices are defined in the virtual table space,
		//	so the model matrix is the identity.
		var theIdentityMatrix = matrix_identity_float4x4
		renderEncoder.setVertexBytes(
			&theIdentityMatrix,
			length: MemoryLayout.size(ofValue: theIdentityMatrix),
			index: BufferIndexModelMatrix)

		renderEncoder.setVertexBuffer(
						checkerboardPoses,
						offset: 0,
						index: BufferIndexTilingMatrices)
		
		func setColoredWall(
			_ wall: CellWall
		) {
		
			renderEncoder.setVertexBuffer(
							wall.vertices,
							offset: 0,
							index: BufferIndexVertexAttributes)

			var theColor = wall.color
			renderEncoder.setFragmentBytes(
				&theColor,
				length: MemoryLayout.size(ofValue: theColor),
				index: BufferIndexColoration)
		}

		//	If checkerboardCellWallsReducedLOD is non-empty,
		//	we'll first draw the high LOD instances
		//	and then draw the low LOD instances.
		
		precondition(
			checkerboardPoses.length % MemoryLayout<simd_float4x4>.stride == 0,
			"checkerboardPoses.length is not an integer multiple of the simd_float4x4 stride." )

		let theTotalPoseCount
			= checkerboardPoses.length
			/ MemoryLayout<simd_float4x4>.stride

		let theHighLODPoseCount = checkerboardCellWallsReducedLOD.isEmpty ?
				theTotalPoseCount : min( 1 + 30, theTotalPoseCount )

		let theLowLODPoseCount = theTotalPoseCount - theHighLODPoseCount
		let theLowLODBaseInstance = theHighLODPoseCount

		for wall in checkerboardCellWalls {
		
			setColoredWall(wall)
			
			let theHighLODBaseInstance
				= wall.mayCoincideWithPoolTable ? 1 : 0
			let theHighLODInstanceCount
				= theHighLODPoseCount - theHighLODBaseInstance

			if theHighLODInstanceCount > 0 {

				renderEncoder.drawPrimitives(
								type: .triangleStrip,
								vertexStart: 0,
								vertexCount: wall.vertexCount,
								instanceCount: theHighLODInstanceCount,
								baseInstance: theHighLODBaseInstance)
			}
		}
		if theLowLODPoseCount > 0 {

			for wall in checkerboardCellWallsReducedLOD {

				setColoredWall(wall)

				renderEncoder.drawPrimitives(
								type: .triangleStrip,
								vertexStart: 0,
								vertexCount: wall.vertexCount,
								instanceCount: theLowLODPoseCount,
								baseInstance: theLowLODBaseInstance)
			}
		}

		renderEncoder.setCullMode(.back)	//	restore our default

		//	Euclidean headset-based message
		//
		//	The message (if any) must get rendered last,
		//	to draw the message on top of all other content,
		//	regardless of depth.
		//
		//		Design note: It would be difficult to insert
		//		the message between other content because
		//		Pool Hall's GPU functions often use radial depth
		//		instead of the traditional planar depth.
		//
		
		if let theMessageTexture = messageTexture,
		   var theMessageUniforms = messageUniforms,
		   var theMessageOpacity = messageOpacity {
		   
			renderEncoder.setVertexBytes(
							&theMessageUniforms,
							length: MemoryLayout.size(ofValue: theMessageUniforms),
							index: MessageBufferIndexUniforms)
			renderEncoder.setRenderPipelineState(messagePipelineState)
			renderEncoder.setDepthStencilState(messageDepthStencilState)
			renderEncoder.setVertexBuffer(
							messageMesh,
							offset: 0,
							index: MessageBufferIndexVertexAttributes)
			renderEncoder.setFragmentBytes(
							&theMessageOpacity,
							length: MemoryLayout.size(ofValue: theMessageOpacity),
							index: BufferIndexOpacity)
			renderEncoder.setFragmentTexture(
							theMessageTexture,
							index: MessageTextureIndex)
			renderEncoder.setFragmentSamplerState(
							messageSampler,
							index: MessageSamplerIndex)
			renderEncoder.drawPrimitives(
							type: .triangleStrip,
							vertexStart: 0,
							vertexCount: 4)
		}

		renderEncoder.popDebugGroup()
	}
}
