unit KM_RenderTerrain_Mobile; {$I KaM_Remake.inc} interface uses SysUtils, CastleGLShaders, CastleGLES, CastleRenderOptions; type ETerrainForeignGLError = class(Exception); ETerrainOwnedGpuError = class(Exception); ETerrainContextUnavailable = class(Exception); TTerrainContextCheck = function: Boolean of object; TTerrainMobileVertex = packed record X, Y, Z: Single; U, V: Single; Opacity: Single; GradientU: Single; end; TTerrainMobileQuad = array[0..3] of TTerrainMobileVertex; TTerrainMobileSampleKind = (tmsAtlasUV, tmsGradientU); TKMTerrainMobileGraphics = class private fProgram: TGLSLProgram; fPendingProgram: TGLSLProgram; fProgramReady: Boolean; fContextCheck: TTerrainContextCheck; fStreamVbo: CastleGLES.GLuint; fStreamStorageReady: Boolean; fRunVertices: array[0..6143] of TTerrainMobileVertex; fRunTexID: Cardinal; fRunVertexCount: Integer; fPassActive: Boolean; fPassSampleKind: TTerrainMobileSampleKind; fPassSourceBlend: TBlendingSourceFactor; fPassDestinationBlend: TBlendingDestinationFactor; fStateApplied: Boolean; fPassFailed: Boolean; procedure AppendQuadVertices(const aQuad: TTerrainMobileQuad); procedure ResetPendingRun; procedure FlushPendingRun; procedure ClosePassState; function ContextCurrent: Boolean; procedure DisposeClosedProgram; public constructor Create; property ContextCheck: TTerrainContextCheck read fContextCheck write fContextCheck; destructor Destroy; override; procedure BeginPass(aSampleKind: TTerrainMobileSampleKind; aSourceBlend: TBlendingSourceFactor; aDestinationBlend: TBlendingDestinationFactor); procedure SubmitQuad(aTextureId: Cardinal; const aQuad: TTerrainMobileQuad); procedure FinishPass; procedure AbortPass; procedure DiscardPendingRunWithoutGL; procedure CreateCurrentProgram; procedure CreateCurrentStreamStorage; procedure ReleaseCurrentGpuObjects; procedure DiscardGpuWithoutGL; procedure ConfirmPreviousProgramClosed; function GpuObjectsComplete: Boolean; end; implementation uses CastleRenderContext, CastleApplicationProperties, KM_Render; const TerrainVertexShader = 'attribute vec3 aPosition;' + #10 + 'attribute vec2 aTexCoord;' + #10 + 'attribute float aOpacity;' + #10 + 'attribute float aGradientCoord;' + #10 + 'uniform mat4 uProjection;' + #10 + 'varying vec2 vTexCoord;' + #10 + 'varying float vOpacity;' + #10 + 'varying float vGradientCoord;' + #10 + 'void main() {' + #10 + ' gl_Position = uProjection * vec4(aPosition, 1.0);' + #10 + ' vTexCoord = aTexCoord;' + #10 + ' vOpacity = aOpacity;' + #10 + ' vGradientCoord = aGradientCoord;' + #10 + '}'; TerrainFragmentShader = 'uniform sampler2D uTexture;' + #10 + 'uniform int uSampleKind;' + #10 + 'varying vec2 vTexCoord;' + #10 + 'varying float vOpacity;' + #10 + 'varying float vGradientCoord;' + #10 + 'void main() {' + #10 + ' vec2 chosenUV = uSampleKind == 0 ? vTexCoord : vec2(vGradientCoord, 0.5);' + #10 + ' gl_FragColor = texture2D(uTexture, chosenUV) * vec4(1.0, 1.0, 1.0, clamp(vOpacity, 0.0, 1.0));' + #10 + '}'; function DrainTerrainGLErrors: string; var ErrorCode: CastleGLES.GLenum; begin Result := ''; if not Assigned(CastleGLES.glGetError) then raise Exception.Create('Terrain GLES glGetError is unavailable'); repeat ErrorCode := CastleGLES.glGetError(); if ErrorCode <> CastleGLES.GL_NO_ERROR then begin if Result <> '' then Result := Result + ', '; Result := Result + '0x' + IntToHex(ErrorCode, 8); end; until ErrorCode = CastleGLES.GL_NO_ERROR; end; procedure RequireTerrainGLErrorFree(const aStage: string); var Errors: string; begin Errors := DrainTerrainGLErrors; if Errors <> '' then raise ETerrainOwnedGpuError.Create('Terrain GLES ' + aStage + ' failed with error(s) ' + Errors); end; constructor TKMTerrainMobileGraphics.Create; begin inherited Create; fProgram := nil; fStreamVbo := 0; fStreamStorageReady := False; fPassActive := False; fStateApplied := False; fPassFailed := False; fRunTexID := 0; fRunVertexCount := 0; end; procedure TKMTerrainMobileGraphics.ConfirmPreviousProgramClosed; begin fProgramReady := False; FreeAndNil(fPendingProgram); FreeAndNil(fProgram); end; procedure TKMTerrainMobileGraphics.DisposeClosedProgram; begin if not ApplicationProperties.IsGLContextOpen then ConfirmPreviousProgramClosed; end; destructor TKMTerrainMobileGraphics.Destroy; begin if ContextCurrent then ReleaseCurrentGpuObjects; DiscardGpuWithoutGL; DisposeClosedProgram; if (fProgram <> nil) or (fPendingProgram <> nil) then raise ETerrainContextUnavailable.Create('Terrain shader disposal awaits completed Castle context close'); inherited; end; procedure TKMTerrainMobileGraphics.CreateCurrentProgram; var Candidate: TGLSLProgram; PositionAttribute, TexCoordAttribute, OpacityAttribute, GradientAttribute: TGLSLAttribute; ProjectionUniform, TextureUniform, SampleKindUniform: TGLSLUniform; begin if fProgramReady and (fProgram <> nil) then Exit; DisposeClosedProgram; if (fProgram <> nil) or (fPendingProgram <> nil) then raise ETerrainContextUnavailable.Create('Terrain shader creation awaits disposal after Castle context close'); if not ContextCurrent then raise ETerrainContextUnavailable.Create('Terrain shader creation requires the accepted current context'); Candidate := nil; try Candidate := TGLSLProgram.Create; Candidate.FragmentShaderRequired := True; Candidate.AttachVertexShader(TerrainVertexShader); Candidate.AttachFragmentShader(TerrainFragmentShader); Candidate.Link; PositionAttribute := Candidate.Attribute('aPosition'); TexCoordAttribute := Candidate.Attribute('aTexCoord'); OpacityAttribute := Candidate.Attribute('aOpacity'); GradientAttribute := Candidate.Attribute('aGradientCoord'); ProjectionUniform := Candidate.Uniform('uProjection', umIgnore); TextureUniform := Candidate.Uniform('uTexture', umIgnore); SampleKindUniform := Candidate.Uniform('uSampleKind', umIgnore); if (PositionAttribute.Location = -1) or (TexCoordAttribute.Location = -1) or (OpacityAttribute.Location = -1) or (GradientAttribute.Location = -1) or (ProjectionUniform.Location = -1) or (TextureUniform.Location = -1) or (SampleKindUniform.Location = -1) then raise Exception.Create('Terrain shader is missing a required attribute or uniform'); if not ContextCurrent then raise ETerrainContextUnavailable.Create('Terrain context changed during shader creation'); fProgram := Candidate; fProgramReady := True; Candidate := nil; finally if Candidate <> nil then begin if ContextCurrent then FreeAndNil(Candidate) else begin fPendingProgram := Candidate; DisposeClosedProgram; end; end; end; end; procedure TKMTerrainMobileGraphics.CreateCurrentStreamStorage; var Candidate: CastleGLES.GLuint; OldErrors: string; begin if fStreamStorageReady then Exit; OldErrors := DrainTerrainGLErrors; if OldErrors <> '' then raise ETerrainForeignGLError.Create('Terrain GLES encountered pre-existing error(s) before stream allocation: ' + OldErrors); if fStreamVbo = 0 then begin Candidate := 0; CastleGLES.glGenBuffers(1, @Candidate); if Candidate = 0 then raise Exception.Create('Terrain GLES failed to allocate a stream buffer name'); fStreamVbo := Candidate; RequireTerrainGLErrorFree('stream buffer name allocation'); end; fStreamStorageReady := False; try RenderContext.BindBuffer[btArray] := fStreamVbo; CastleGLES.glBufferData(CastleGLES.GL_ARRAY_BUFFER, 6144 * SizeOf(TTerrainMobileVertex), nil, CastleGLES.GL_STREAM_DRAW); if ContextCurrent then RenderContext.BindBuffer[btArray] := 0; RequireTerrainGLErrorFree('stream storage allocation'); fStreamStorageReady := True; except try if ContextCurrent then RenderContext.BindBuffer[btArray] := 0; except { Preserve the allocation failure. } end; raise; end; end; function TKMTerrainMobileGraphics.ContextCurrent: Boolean; begin Result := Assigned(fContextCheck) and fContextCheck(); end; procedure TKMTerrainMobileGraphics.ReleaseCurrentGpuObjects; var FirstError: Exception; procedure RememberError; begin if FirstError = nil then FirstError := Exception(AcquireExceptionObject); end; begin FirstError := nil; try if ContextCurrent and fPassActive then AbortPass; except RememberError; end; try if ContextCurrent and (fStreamVbo <> 0) then begin CastleGLES.glDeleteBuffers(1, @fStreamVbo); fStreamVbo := 0; end; except RememberError; end; try if ContextCurrent then begin FreeAndNil(fPendingProgram); if ContextCurrent then FreeAndNil(fProgram); end; except RememberError; end; DiscardGpuWithoutGL; if FirstError <> nil then raise FirstError; end; procedure TKMTerrainMobileGraphics.DiscardGpuWithoutGL; begin fProgramReady := False; DisposeClosedProgram; fStreamVbo := 0; fStreamStorageReady := False; ResetPendingRun; fPassActive := False; fStateApplied := False; fPassFailed := False; end; function TKMTerrainMobileGraphics.GpuObjectsComplete: Boolean; begin Result := fProgramReady and (fProgram <> nil) and fStreamStorageReady and (fStreamVbo <> 0); end; procedure TKMTerrainMobileGraphics.ResetPendingRun; begin fRunTexID := 0; fRunVertexCount := 0; end; procedure TKMTerrainMobileGraphics.AppendQuadVertices(const aQuad: TTerrainMobileQuad); const TriangleOrder: array[0..5] of Byte = (0, 1, 2, 0, 2, 3); var I: Integer; begin for I := 0 to High(TriangleOrder) do fRunVertices[fRunVertexCount + I] := aQuad[TriangleOrder[I]]; Inc(fRunVertexCount, Length(TriangleOrder)); end; procedure TKMTerrainMobileGraphics.BeginPass(aSampleKind: TTerrainMobileSampleKind; aSourceBlend: TBlendingSourceFactor; aDestinationBlend: TBlendingDestinationFactor); begin if fPassActive then raise Exception.Create('Terrain graphics pass is already active'); fPassSampleKind := aSampleKind; fPassSourceBlend := aSourceBlend; fPassDestinationBlend := aDestinationBlend; fPassActive := True; fStateApplied := False; fPassFailed := False; ResetPendingRun; end; procedure TKMTerrainMobileGraphics.SubmitQuad(aTextureId: Cardinal; const aQuad: TTerrainMobileQuad); begin if not fPassActive then raise Exception.Create('Terrain graphics SubmitQuad requires an active pass'); if fPassFailed then raise Exception.Create('Terrain graphics pass has already failed'); if aTextureId = 0 then begin fPassFailed := True; raise Exception.Create('Terrain graphics quad has an invalid texture identity'); end; if not fProgramReady or (fProgram = nil) or (fStreamVbo = 0) then begin fPassFailed := True; raise Exception.Create('Terrain graphics shader or stream buffer is unavailable'); end; if fRunVertexCount = 0 then fRunTexID := aTextureId else if fRunTexID <> aTextureId then begin try FlushPendingRun; except fPassFailed := True; raise; end; fRunTexID := aTextureId; end; if fRunVertexCount + 6 > Length(fRunVertices) then begin try FlushPendingRun; except fPassFailed := True; raise; end; fRunTexID := aTextureId; end; AppendQuadVertices(aQuad); end; procedure TKMTerrainMobileGraphics.FlushPendingRun; var PositionAttribute, TexCoordAttribute, OpacityAttribute, GradientAttribute: TGLSLAttribute; ProjectionUniform, TextureUniform, SampleKindUniform: TGLSLUniform; PositionEnabled, TexCoordEnabled, OpacityEnabled, GradientEnabled: Boolean; RunFailed: Boolean; OldErrors: string; procedure CleanupRun; begin try if ContextCurrent and GradientEnabled then GradientAttribute.DisableArray; finally try if ContextCurrent and OpacityEnabled then OpacityAttribute.DisableArray; finally try if ContextCurrent and TexCoordEnabled then TexCoordAttribute.DisableArray; finally try if ContextCurrent and PositionEnabled then PositionAttribute.DisableArray; finally if ContextCurrent then RenderContext.BindBuffer[btArray] := 0; end; end; end; end; end; begin if fRunVertexCount = 0 then Exit; if not fPassActive or fPassFailed then raise Exception.Create('Terrain graphics cannot flush a failed or closed pass'); if not fProgramReady or (fProgram = nil) or (fStreamVbo = 0) then raise Exception.Create('Terrain graphics shader or stream buffer is unavailable'); OldErrors := DrainTerrainGLErrors; if OldErrors <> '' then raise ETerrainForeignGLError.Create('Terrain GLES encountered pre-existing error(s) before flush: ' + OldErrors); if not fStateApplied then begin RenderContext.BlendingEnable(fPassSourceBlend, fPassDestinationBlend); fStateApplied := True; end; PositionEnabled := False; TexCoordEnabled := False; OpacityEnabled := False; GradientEnabled := False; RunFailed := True; try TKMRender.BindTexture(fRunTexID); fProgram.Enable; ProjectionUniform := fProgram.Uniform('uProjection', umIgnore); TextureUniform := fProgram.Uniform('uTexture', umIgnore); SampleKindUniform := fProgram.Uniform('uSampleKind', umIgnore); if (ProjectionUniform.Location = -1) or (TextureUniform.Location = -1) or (SampleKindUniform.Location = -1) then raise Exception.Create('Terrain shader is missing a required draw uniform'); ProjectionUniform.SetValue(RenderContext.ProjectionMatrix); TextureUniform.SetValue(0); SampleKindUniform.SetValue(Ord(fPassSampleKind)); RenderContext.BindBuffer[btArray] := fStreamVbo; PositionAttribute := fProgram.Attribute('aPosition'); TexCoordAttribute := fProgram.Attribute('aTexCoord'); OpacityAttribute := fProgram.Attribute('aOpacity'); GradientAttribute := fProgram.Attribute('aGradientCoord'); PositionAttribute.EnableArrayVector3(nil, 28, PtrUInt(0)); PositionEnabled := True; TexCoordAttribute.EnableArrayVector2(nil, 28, PtrUInt(12)); TexCoordEnabled := True; OpacityAttribute.EnableArraySingle(nil, 28, PtrUInt(20)); OpacityEnabled := True; GradientAttribute.EnableArraySingle(nil, 28, PtrUInt(24)); GradientEnabled := True; RequireTerrainGLErrorFree('shader/bind/attribute setup'); CastleGLES.glBufferData(CastleGLES.GL_ARRAY_BUFFER, fRunVertexCount * SizeOf(TTerrainMobileVertex), @fRunVertices[0], CastleGLES.GL_STREAM_DRAW); RequireTerrainGLErrorFree('stream upload'); CastleGLES.glDrawArrays(CastleGLES.GL_TRIANGLES, 0, CastleGLES.GLsizei(fRunVertexCount)); RequireTerrainGLErrorFree('triangle draw'); ResetPendingRun; RunFailed := False; finally if RunFailed then begin try CleanupRun; except { Preserve the original flush error after attempting every cleanup. } end; end else CleanupRun; end; end; procedure TKMTerrainMobileGraphics.ClosePassState; begin try if ContextCurrent and fStateApplied and (fProgram <> nil) and (fStreamVbo <> 0) then begin try if ContextCurrent then TGLSLProgram.Disable; finally try if ContextCurrent then TKMRender.BindTexture(0); finally try if ContextCurrent then RenderContext.BlendingEnable(bsSrcAlpha, bdOneMinusSrcAlpha); finally if ContextCurrent then RenderContext.BindBuffer[btArray] := 0; end; end; end; end; finally ResetPendingRun; fPassActive := False; fStateApplied := False; fPassFailed := False; end; end; procedure TKMTerrainMobileGraphics.FinishPass; begin if not fPassActive then raise Exception.Create('Terrain graphics FinishPass requires an active pass'); try if fPassFailed then raise Exception.Create('Terrain graphics pass has already failed'); FlushPendingRun; except fPassFailed := True; try ClosePassState; except { Keep the original submission error. } end; raise; end; ClosePassState; end; procedure TKMTerrainMobileGraphics.AbortPass; begin if not fPassActive then Exit; ClosePassState; end; procedure TKMTerrainMobileGraphics.DiscardPendingRunWithoutGL; begin ResetPendingRun; fPassActive := False; fStateApplied := False; fPassFailed := False; end; end.