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

import SwiftUI
import CompositorServices


struct GGVRCompositorLayerConfiguration: CompositorLayerConfiguration {

	func makeConfiguration(
		capabilities: LayerRenderer.Capabilities,
		configuration: inout LayerRenderer.Configuration
	) {

		//	On visionOS, the color space is always Extended Linear Display P3.
		//
		//	As of visionOS 1.1.1 (March 2024)
		//
		//		The supported colorFormats are
		//			rgba8Unorm_srgb
		//			bgra8Unorm_srgb
		//			rgba16Unorm
		//			bgr10a2Unorm
		//			rgba16Float
		//
		//		The supported depthFormats
		//			depth32Float
		//			depth32Float_stencil8
		//
		//		The default colorFormat is
		//			rgba16Float  (contrary to what was said in WWDC 2023 #10089)
		//
		//		The default depthFormat is
		//			depth32Float
		//
		//	We don't need high dynamic range (HDR), so let's use
		//	the 32-bit bgr10a2Unorm instead of the default 64-bit rgba16Float,
		//	to halve the size of our color buffer and the associated bus traffic.
		//	(Although if there were ever an advantage to using rgba16Float it would
		//	be fine -- the app spends only about 1 ms in the fragment function
		//	either way.) Happily bgr10a2Unorm and rgba16Float both avoid
		//	sRGB encoding (gamma encoding) which makes it easy to avoid
		//	the color errors that mismatched encoding assumptions can lead to.
		//
		//	Here's some code to check supported formats:
		//
		//		print("supported colorFormats \(capabilities.supportedColorFormats.map() { pf in pf.rawValue})")
		//		print("supported depthFormats \(capabilities.supportedDepthFormats.map() { df in df.rawValue})")
		//		print("default colorFormat \(configuration.colorFormat)")
		//		print("default depthFormat \(configuration.depthFormat)")
		//
		configuration.depthFormat = .depth32Float	//	this is the default (rawValue: 252)
		configuration.colorFormat = .bgr10a2Unorm	//	default is rgba16Float  (rawValue: 115)
		
		//	Note: In Curved-Spaces Pool Hall, hopes of removing some
		//	residual flickering of the billiards table's procedural texture
		//	in my peripheral vision, I tried disabling foveation
		//
		//		let supportedLayouts = capabilities.supportedLayouts(options: [])
		//		configuration.layout = supportedLayouts.contains(.layered) ? .layered : .dedicated
		//		configuration.isFoveationEnabled = false
		//
		//	The flickering did indeed go away in my peripheral vision,
		//	but much to my surprise it got worse in the whole central part
		//	of my vision. Even more surprising, the billiard balls' outlines
		//	became very pixelated and jumpy, as if antialiasing had been turned off
		//	and/or the whole image were being drawn at a lower resolution.
		//	So let's stick with the foveation, which gives good results
		//	in the user's whole field of view most of the time, and
		//	in the central part all the time.
		let supportsFoveation = capabilities.supportsFoveation
		let supportedLayouts = capabilities.supportedLayouts(
			options: supportsFoveation ? [.foveationEnabled] : [])
		configuration.layout = supportedLayouts.contains(.layered) ? .layered : .dedicated
		configuration.isFoveationEnabled = supportsFoveation
	}
}

extension LayerRenderer.Clock.Instant.Duration {

	var timeInterval: TimeInterval {
	
		let theIntegerSeconds = TimeInterval(components.seconds)
		let theFractionalSeconds = TimeInterval(components.attoseconds) * TimeInterval(1.0e-18)
		let theTimeInterval = theIntegerSeconds + theFractionalSeconds
		
		return theTimeInterval
	}
}

final class GGVRRendererTaskExecutor: TaskExecutor {

	private let queue = DispatchQueue(label: "RenderThreadQueue", qos: .userInteractive)

	func enqueue(_ job: UnownedJob) {
		queue.async {
			job.runSynchronously(on: self.asUnownedSerialExecutor())
		}
	}

	func asUnownedSerialExecutor() -> UnownedTaskExecutor {
		return UnownedTaskExecutor(ordinary: self)
	}

	static let shared: GGVRRendererTaskExecutor = GGVRRendererTaskExecutor()
}

enum GGVRAzimuthalProjection: String, CaseIterable, Identifiable, Codable {

	//	Central projection projects the game's contents
	//	from the (typically curved) virtual space radially
	//	onto the tangent space at the north pole. In practice,
	//	that works great with no correction factors needed.
	//
	//	However, as explained in the article,
	//
	//		State-of-the-art rendering in curved-space virtual reality
#warning("Include a link to the article, if that comes to pass.")
	//
	//	for full mathematical correctness, ggvrMakeViewProjection()
	//	must compress/stretch the projected image in the x direction by
	//
	//		cos(δ)		in the spherical case
	//		1			in the Euclidean case
	//		cosh(δ)		in the hyperbolic case
	//
	//	and stretch/compess it in all directions by
	//
	//		δ/sin(δ)	in the spherical case
	//		1			in the Euclidean case
	//		δ/sinh(δ)	in the hyperbolic case
	//
	//	where
	//		d is the "pupillary distance", that is,
	//			the distance from the bridge of the player's nose
	//			to the center of the given eye's pupil, in meters,
	//		ρ is gSpaceRadius, in meters, and
	//		δ = d/ρ is the nose-to-eye angle, in radians (dimensionless).
	//
	//	The stretched/compressed central projection has two great features:
	//
	//		Feature #1. It takes geodesics to geodesics, so once
	//		a triangle's three vertices have been placed,
	//		the triangle's whole interior ends up in the right place.
	//
	//		Feature #2. It preserves angles at each of the player's eyes.
	//		Combined with Feature #1, this implies that the player
	//		sees each object in the same direction in the Euclidean
	//		projected image that s/he would see it in the original
	//		curved space. Note that they don't perceive correct radial depths,
	//		but they do perceive the same incorrect radial depths
	//		that their Euclidean-trained stereoscopic vision would
	//		make them perceive in the space itself.
	//
	//	The stretched/compressed central projection also has two flaws:
	//
	//		Flaw #1. In the spherical case, the central projection
	//		goes only as far as the equator. Given that stretched/compressed
	//		central projection shows each object as the player would
	//		see it in the original curved space, this flaw is inevitable
	//		because the player would need to look walleyed to focus
	//		on objects in the southern hemisphere. We wouldn't want
	//		to force our players to look walleyed in any case, to spare
	//		them the discomfort (or worse).
	//
	//		Flaw #2. Players see hyperbolic space the way
	//		a Euclidean-born-and-raised visitor would see it.
	//		It feels very small and cramped, even though in reality
	//		hyperbolic space is large and spacious.
	//
	//	Implementation note: For typical values like d = 0.032 m and ρ = 0.6 m,
	//
	//		cos(δ)    ≈ 0.9986
	//		δ/sin(δ)  ≈ 1.0005
	//
	//		cosh(δ)   ≈ 1.0014
	//		δ/sinh(δ) ≈ 1.9995
	//
	//	so we may ignore the stretch/compression with negligible effect,
	//	and just do a plain central projection instead.
	//
	case central
	
	//	The azimuthal equidistant projection has a great feature:
	//
	//		Feature. Using their Euclidean-trained stereoscopic vision,
	//		players see each object at its correct radial distance
	//		into the scene.
	//
	//	It also has a flaw:
	//
	//		Flaw: The equidistant projection does *not* take
	//		geodesics to geodesics, so even once a triangle's three vertices
	//		have been placed, the triangle's interior may in effect
	//		bend forwards or backwards, thus revealing other objects
	//		that it shouldn't reveal or hiding other objects that it
	//		shouldn't hide. This effect can be severe for a tiny triangle
	//		near the player's antipodal point, which maps to a huge triangle
	//		sitting in front of the player's face. Fortunately there's
	//		an easy workaround: In the spherical case our vertex shader
	//		uses radial depth instead of planar depth.
	//
	//	The azimuthal equidistant projection is the best choice
	//	in almost all situations. Games should set it as the default.
	//
	case equidistant

	var id: String { self.rawValue }
	
	var titleInPicker: String {
	
		switch self {
		case .central:		return "Tourist"
		case .equidistant:	return "Native"
		}
	}
	
	var gpuProjectionCode: UInt8 {
	
		//	Let's pass the GGVRAzimuthalProjection to the GPU
		//	as a UInt8, which the vertex function function
		//	may interpret as an ASCII character.
		//
		//		Note:  This information could also be
		//		passed as an enum, but I hestitate to define
		//		a shared enum in GGVR_GPU_Definitions.h
		//		for two reasons:
		//
		//		1. It can be a little delicate to ensure
		//		that the Swift code and the Metal code
		//		both see the enum as the same size integer.
		//
		//		2. I'm not eager to define a second enum
		//		that might cause confusion with the present
		//		GGVRAzimuthalProjection, which is a Swift enum.
		//
	
		switch self {
		case .central:		return UInt8(ascii: "c")
		case .equidistant:	return UInt8(ascii: "e")
		}
	}
}

func ggvrPipelineState(
	layerRenderer: LayerRenderer,
	vertexFunction: MTLFunction,
	fragmentFunction: MTLFunction,
	vertexDescriptor: MTLVertexDescriptor,
	enableBlending: Bool
) -> MTLRenderPipelineState? {

	let thePipelineDescriptor = MTLRenderPipelineDescriptor()
	thePipelineDescriptor.label
		= "Geometry Games VR render pipeline for \(fragmentFunction.label ?? "???")"
//	thePipelineDescriptor.sampleCount = ???
//		presumably visionOS doesn't do multisampling
	thePipelineDescriptor.vertexFunction = vertexFunction
	thePipelineDescriptor.fragmentFunction = fragmentFunction
	thePipelineDescriptor.vertexDescriptor = vertexDescriptor
	thePipelineDescriptor.colorAttachments[0].pixelFormat = layerRenderer.configuration.colorFormat
	thePipelineDescriptor.depthAttachmentPixelFormat = layerRenderer.configuration.depthFormat
	thePipelineDescriptor.stencilAttachmentPixelFormat = MTLPixelFormat.invalid
	thePipelineDescriptor.maxVertexAmplificationCount = layerRenderer.properties.viewCount

	thePipelineDescriptor.colorAttachments[0].isBlendingEnabled = enableBlending
	if enableBlending {
	
		//	We define textures using pre-multiplied colors (αR, αG, αB, α)
		//	instead of the traditional (R,G,B,α). This makes blending easier
		//	and, even though we're not manually generating any mipmaps here,
		//	it makes mipmap generation easier too. Here are the details:
		//
		//	Blending
		//
		//		Rigorously correct blending requires
		//
		//					   αs*(Rs,Gs,Bs) + (1 - αs)*αd*(Rd,Gd,Bd)
		//			(R,G,B) = ----------------------------------------
		//							 αs      +      (1 - αs)*αd
		//
		//				  α = αs + (1 - αs)*αd
		//
		//		Replacing the traditional (R,G,B,α) with the premultiplied (αR,αG,αB,α)
		//		simplifies the formula to
		//
		//			(αR, αG, αB) = (αs*Rs, αs*Gs, αs*Bs) + (1 - αs)*(αd*Rd, αd*Gd, αd*Bd)
		//
		//					   α = αs + (1 - αs)*αd
		//
		//		Because they share the same coefficients,
		//		we may merge the RGB and α parts into a single formula
		//
		//			(αR, αG, αB, α)
		//				=     1    * (αs*Rs, αs*Gs, αs*Bs, αs)
		//				+ (1 - αs) * (αd*Rd, αd*Gd, αd*Bd, αd)
		//
		//	Mipmap generation (not needed here)
		//
		//		When generating mipmaps, to average two (or more) pixels correctly
		//		we must weight them according to their alpha values.
		//		With traditional (R,G,B,α) the formula is a bit messy
		//
		//								 α0*(R0,G0,B0) + α1*(R1,G1,B1)
		//			(Ravg, Gavg, Bavg) = -----------------------------
		//								            α0 + α1
		//
		//								 α0 + α1
		//						  αavg = -------
		//									2
		//
		//		With premultiplied (αR,αG,αB,α) the formula becomes a simple average
		//
		//			(αavg*Ravg, αavg*Gavg, αavg*Bavg, αavg)
		//
		//				  (α0*R0, α0*G0, α0*B0, α0) + (α1*R1, α1*G1, α1*B1, α1)
		//				= -----------------------------------------------------
		//											2
		//	Performance note:  When drawing opaque content using
		//	tile-based deferred rendering, the GPU calls the fragment function
		//	only for visible fragments that aren't eclipsed by anything
		//	closer to the observer. But with partially transparent content,
		//	the GPU will need to call a fragment function for each primitive
		//	that may affect the pixel in question.
		//
		//	Set the values define above in the formula for (αR, αG, αB, α).
		thePipelineDescriptor.colorAttachments[0].rgbBlendOperation = .add
		thePipelineDescriptor.colorAttachments[0].alphaBlendOperation = .add
		thePipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = .one
		thePipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = .one
		thePipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
		thePipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
	}

	do {
		let thePipelineState
			= try layerRenderer.device.makeRenderPipelineState(	//	returns non-nil
										descriptor: thePipelineDescriptor)
		return thePipelineState
	} catch {
		assertionFailure("Couldn't create the pipeline state for \(vertexFunction.label ?? "???") and \(fragmentFunction.label ?? "???"): \(error)")
		return nil
	}
}

func ggvrFogCutoff(
	maxTranslationInRadians: Double,
	spaceGeometry: SpaceGeometry,
	azimuthalProjection: GGVRAzimuthalProjection
) -> Double {	//	returns the fog cutoff distance in meters
	
	let ρ = spaceGeometry.spaceRadius
	
	switch azimuthalProjection {

	case .central:
	
		switch spaceGeometry.geometryType {
		
		case .spherical:
#warning("If I move to trigonometric shading, this becomes irrelevant?")
			//	In spherical geometry, central projection maps
			//	the northern hemisphere onto the whole tangent space,
			//	so in principle the radius is infinite. But we'll get
			//	reasonable shading with an arbitrary value like 8 radians.
			return 8.0 * ρ
		
		case .euclidean:
			//	In Euclidean space, the central and azimuthal projections
			//	are both the identity.
			return 1.125 * maxTranslationInRadians * ρ
		
		case .hyperbolic:
			//	In hyperbolic geometry, central projection maps
			//	the whole hyperboloid onto a disk of radius 1 radian.
			return 1.125 * 1.0 * ρ
		}

	case .equidistant:
	
		//	maxTranslationInRadians is the greatest distance (in radians)
		//	that any tiling matrix translates the basepoint (0,0,0,ρ).
		//	Parts of the translated cell will, of course, be more distant
		//	than its center, and as the player wanders about in the center cell,
		//	the distance may increase futher. In any case we don't want the fog
		//	to reach 100% until, say, about 1.125 times the maximum distance,
		//	to ensure that the player can always see -- at least dimly --
		//	all rendered copies of the cell. We'd be willing to tolerate
		//	some faint popping around the edge in order to ensure that
		//	no rendered copies go to waste.
		
		return 1.125 * maxTranslationInRadians * ρ
	}
}

func ggvrDepthStencilState(
	device: MTLDevice,
	depthCompareEnabled: Bool,
	depthWriteEnabled: Bool
) -> MTLDepthStencilState? {

	let theDepthStencilDescriptor = MTLDepthStencilDescriptor()

	theDepthStencilDescriptor.depthCompareFunction
		= depthCompareEnabled ? MTLCompareFunction.greater : MTLCompareFunction.always
	
	theDepthStencilDescriptor.isDepthWriteEnabled = depthWriteEnabled

	let theDepthStencilState = device.makeDepthStencilState(
								descriptor: theDepthStencilDescriptor)
	assert(
		theDepthStencilState != nil,
		"Couldn't create theDepthStencilState for depthCompareEnabled = \(depthCompareEnabled) and depthWriteEnabled = \(depthWriteEnabled)")
		
	return theDepthStencilState
}

func ggvrMeshFacets(
	device: MTLDevice,
	vertexIndices: [UInt16]	//	each set of 3 vertices defines a triangle
) -> MTLBuffer {

	precondition(
		vertexIndices.count % 3 == 0,
		"The number of vertex indices in a triangle mesh must be a multiple of 3.")

	guard let theIndexBuffer = device.makeBuffer(
		bytes: vertexIndices,
		length: vertexIndices.count * MemoryLayout<UInt16>.stride,
		options: [MTLResourceOptions.storageModeShared]
	) else {
		fatalError("Couldn't make the theIndexBuffer in ggvrMeshFacets()")
	}
	
	return theIndexBuffer
}

func ggvrSamplerState(
	device: MTLDevice,
	sMode: MTLSamplerAddressMode,
	tMode: MTLSamplerAddressMode
) -> MTLSamplerState? {
	
	let theDescriptor = MTLSamplerDescriptor()

	theDescriptor.normalizedCoordinates = true

	theDescriptor.sAddressMode = sMode
	theDescriptor.tAddressMode = tMode

	theDescriptor.minFilter = MTLSamplerMinMagFilter.linear
	theDescriptor.magFilter = MTLSamplerMinMagFilter.linear
	theDescriptor.mipFilter = MTLSamplerMipFilter.linear
	
	theDescriptor.maxAnisotropy = 16	//	= 1 to disable anisotropic adjustments

	let theSamplerState = device.makeSamplerState(descriptor: theDescriptor)

	return theSamplerState
}

func ggVRComputeFrameInterval(
	predictedPresentationTime: TimeInterval,
	previousPresentationTime: TimeInterval?
) -> Double {

	//	Compute the frame interval, which should be
	//	1/90 sec on a Vision Pro or 1/60 sec in the simulator.

	guard let thePreviousPresentationTime = previousPresentationTime
	else { return 1.0/90.0 } // should occur only once, on the very first frame
	
	let theFrameInterval = predictedPresentationTime - thePreviousPresentationTime
	
	return theFrameInterval
}

func ggvrRenderPassDescriptor(
	layerRenderer: LayerRenderer,
	drawable: LayerRenderer.Drawable
) -> MTLRenderPassDescriptor {

	let theRenderPassDescriptor = MTLRenderPassDescriptor()

	let theColorAttachmentDescriptor: MTLRenderPassColorAttachmentDescriptor
		= theRenderPassDescriptor.colorAttachments[0]
	theColorAttachmentDescriptor.texture = drawable.colorTextures[0]
	theColorAttachmentDescriptor.loadAction = .clear
	theColorAttachmentDescriptor.storeAction = .store
	theColorAttachmentDescriptor.clearColor
		= MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0)
	
	let theDepthAttachmentDescriptor: MTLRenderPassDepthAttachmentDescriptor
		= theRenderPassDescriptor.depthAttachment
	theDepthAttachmentDescriptor.texture = drawable.depthTextures[0]
	theDepthAttachmentDescriptor.loadAction = .clear
	theDepthAttachmentDescriptor.storeAction = .store
		//	Unlike on iOS, on visionOS the storeAction must be .store.
		//	If we instead set .dontCare, the whole display would be pure black.
		//	Maybe visionOS uses the depth buffer in its post-processing?
	theDepthAttachmentDescriptor.clearDepth = 0.0	//	assumes z axis points towards the player

	theRenderPassDescriptor.rasterizationRateMap = drawable.rasterizationRateMaps.first
	
	if layerRenderer.configuration.layout == .layered {
		theRenderPassDescriptor.renderTargetArrayLength = drawable.views.count
	}
	
	return theRenderPassDescriptor
}

func ggvrMakeViewProjection(
	virtualHeadsetPose: simd_double4x4,
	drawable: LayerRenderer.Drawable,
	spaceGeometry: SpaceGeometry
) -> GGVRViewProjection {

	let theGameSpaceToHeadsetSpace = convertDouble4x4toFloat4x4(
		geometricInverse(
			of: virtualHeadsetPose,
			geometryType: spaceGeometry.geometryType))

	//	Our GPU vertex function (= shader) maps the game's contents
	//	onto the tangent space based at the "bridge of the player's nose",
	//	that is, at the midpoint between the player's eyes.
	//	If that point were significantly offset from the headset's nominal position,
	//	we could explicitly factor in a headsetToBridgeOfNose matrix.
	//	But in practice the bridge of the nose differs from the headset's
	//	nominal position by only a few millimeters. For example,
	//	in a test run, visionOS measured my eye offsets to be
	//
	//		left eye:  ( -32 mm, -3 mm, +3 mm )
	//		right eye: ( +36 mm, -3 mm, +6 mm )
	//
	//	So we may let the GPU base the tangent space at the headset's
	//	nominal position without compromising the result.
	//
	//		Note: The bridge-of-nose space is still
	//		in the (typically curved) virtual space,
	//		before the azimuthal projection.
	//
	let theGameSpaceToBridgeOfNoseSpace = theGameSpaceToHeadsetSpace

	//	The vertex shader will apply either a central projection
	//	or an azimuthal projection. Either way, it maps the space
	//	into the Euclidean tangent space at the north pole.
	//	While our virtual tangent space sits at w = spaceRadius,
	//	Compositor Services assumes a physical Euclidean space
	//	at w = 1 meter. So we must rescale.
	//
	let	ρPhysical = 1.0							//	in meters
	let ρVirtual  = spaceGeometry.spaceRadius	//	in meters
	let theVirtualTangentSpaceToPhysicalTangentSpace
		= simd_double4x4(diagonal:
			SIMD4<Double>(1.0, 1.0, 1.0, ρPhysical/ρVirtual))
	
	//	As explained in the long comment in GGVRAzimuthalProjection,
	//	for full mathematical correctness the vertex shader would need
	//	to multiply the x coordinate by theCentralProjectionAspectRatio
	//	and all coordinates by theCentralProjectionScaleFactor.
	//	In practice, though, the correction factors are tiny and we may
	//	safely omit them, with negligible effects on the rendered image.
	//	This paragraph of code is included only to illustrate what would
	//	be required for mathematical perfection.  :-)
	//
	//	Note #1: It's actually a little more complicated than that
	//	if the eyes don't sit exactly to the left and right
	//	of the headset's basepoint, but the conclusion remains
	//	the same because sin δ ≈ tan δ ≈ δ and cos δ ≈ 1, and
	//	similarly for the hyperbolic functions.
	//
	//	Note #2: Even though we compute these corrections factors in all cases,
	//	the shader ignores them when using an equidistant projection.
	//
	let theCentralProjectionAspectRatio: Double
	let theCentralProjectionScaleFactor: Double
	if drawable.views.count == 2 {

		let theBridgeOfNoseToEyeDistance = 0.5 * Double(
			abs(drawable.views[0].transform[3][0])
		  + abs(drawable.views[1].transform[3][0]) )

		let δ = theBridgeOfNoseToEyeDistance / ρVirtual // pupillary angle in radians
		
		switch spaceGeometry.geometryType {
		case .spherical:	theCentralProjectionAspectRatio = cos(δ)
		case .euclidean:	theCentralProjectionAspectRatio = 1.0
		case .hyperbolic:	theCentralProjectionAspectRatio = cosh(δ)
		}
		switch spaceGeometry.geometryType {
		case .spherical:	theCentralProjectionScaleFactor = δ/sin(δ)
		case .euclidean:	theCentralProjectionScaleFactor = 1.0
		case .hyperbolic:	theCentralProjectionScaleFactor = δ/sinh(δ)
		}

	} else {
		theCentralProjectionAspectRatio = 1.0
		theCentralProjectionScaleFactor = 1.0
	}
			

	let theVirtualTangentSpaceToClipSpace: [simd_float4x4]
		= (0 ..< drawable.views.count).map() { eyeIndex in

		let theEyeInfo: LayerRenderer.Drawable.View = drawable.views[eyeIndex]

		//	The theEyeInfo.transform is the eye's pose relative to the headset
		//	in the physical Euclidean space with space radius ρPhysical = 1 meter.
		let theEyeSpaceToPhysicalTangentSpace
			= convertFloat4x4toDouble4x4(theEyeInfo.transform)

		let thePhysicalTangentSpaceToEyeSpace
			= geometricInverse(
				of: theEyeSpaceToPhysicalTangentSpace,
				geometryType: .euclidean)
								
		//	visionOS uses off-axis viewing, as one would expect with the player's
		//	left and right eyes focusing on the center of the player's field of view.
		//	The Drawable gives us a ready-to-use Euclidean projection matrix,
		//	still assuming ρPhysical = 1 of course.
		let theProjectionMatrix = convertFloat4x4toDouble4x4( 	drawable.computeProjection(viewIndex: eyeIndex))

		let theComposition = convertDouble4x4toFloat4x4(
			theProjectionMatrix	//	right-to-left matrix action
			* thePhysicalTangentSpaceToEyeSpace
			* theVirtualTangentSpaceToPhysicalTangentSpace)
		
		return theComposition
	}
	
	let theViewProjection = GGVRViewProjection(

		gameSpaceToBridgeOfNoseSpace: theGameSpaceToBridgeOfNoseSpace,

		xFactor:  Float(theCentralProjectionAspectRatio * theCentralProjectionScaleFactor),
		yzFactor: Float(theCentralProjectionScaleFactor),

		tangentSpaceToClipSpace: (
			theVirtualTangentSpaceToClipSpace[0],
			theVirtualTangentSpaceToClipSpace.count >= 2 ?
				theVirtualTangentSpaceToClipSpace[1] :	//	on device
				matrix_identity_float4x4)				//	on simulator
		)

	return theViewProjection
}

func ggvrEncodeViewports(
	drawable: LayerRenderer.Drawable,
	renderEncoder: MTLRenderCommandEncoder
) {

	let theViewports = drawable.views.map() { view in view.textureMap.viewport }
	renderEncoder.setViewports(theViewports)

	let theNumViews = drawable.views.count
	if theNumViews > 1 {
	
		var theViewMappings = (0 ..< theNumViews).map() { i in
			MTLVertexAmplificationViewMapping(
				viewportArrayIndexOffset: UInt32(i),
				renderTargetArrayIndexOffset: UInt32(i))
		}
		
		renderEncoder.setVertexAmplificationCount(
			theViewports.count,
			viewMappings: &theViewMappings)
	}
}
