-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathGPUCopyConsistencyExample.hs
More file actions
373 lines (339 loc) · 19.4 KB
/
GPUCopyConsistencyExample.hs
File metadata and controls
373 lines (339 loc) · 19.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- |
-- Example : GPUCopyConsistency
-- Description : Demonstrates switching vertex and texture data for drawing via GPU-to-GPU copies.
-- Copyright : (c) Kyle Lukaszek, 2025
-- License : BSD3
--
-- Based on the SDL_gpu_examples/CopyConsistency C example.
-- This example shows:
-- - Creating multiple source vertex buffers (LeftVertexBuffer, RightVertexBuffer) and source textures (LeftTexture, RightTexture).
-- - Creating a common "active" VertexBuffer and Texture that the graphics pipeline will use for drawing.
-- - In the render loop, before drawing each half of the screen:
-- - Using SDL_CopyGPUBufferToBuffer to copy data from either LeftVertexBuffer or RightVertexBuffer
-- into the common VertexBuffer.
-- - Using SDL_CopyGPUTextureToTexture to copy data from either LeftTexture or RightTexture
-- into the common Texture.
-- - Drawing a textured quad using the (now updated) common VertexBuffer and Texture.
-- - This technique allows for dynamic resource switching on the GPU without re-binding different
-- objects to the pipeline in the render pass.
-- |
module Main where
import Control.Exception (bracket, bracketOnError, finally)
import Control.Monad (unless, void, when, (>=>))
-- Only for shouldQuitRef
import Data.Bits ((.|.))
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.Maybe (catMaybes, fromJust, fromMaybe, isJust, isNothing)
import Data.Word (Word16, Word32, Word64)
import Foreign.C.Types (CFloat, CSize, CUInt)
import Foreign.Marshal.Array (pokeArray, withArray)
import Foreign.Marshal.Utils (copyBytes, with)
import Foreign.Ptr (Ptr, castPtr, nullPtr, plusPtr)
import Foreign.Storable (peek, poke, pokeByteOff, sizeOf)
import GPUCommon
import SDL3
import System.Exit (exitFailure, exitSuccess)
import System.FilePath ((</>))
-- Vertex Data for Left Quad (maps to ravioli.bmp)
-- Coords map from -1 to 0 on X
leftVertexData :: [PositionTextureVertex]
leftVertexData =
[ PositionTextureVertex (-1.0) 1.0 0 0 0, -- Top-left
PositionTextureVertex 0.0 1.0 0 1 0, -- Top-right (of left quad)
PositionTextureVertex 0.0 (-1.0) 0 1 1, -- Bottom-right (of left quad)
PositionTextureVertex (-1.0) (-1.0) 0 0 1 -- Bottom-left
]
-- Vertex Data for Right Quad (maps to ravioli_inverted.bmp)
-- Coords map from 0 to 1 on X
rightVertexData :: [PositionTextureVertex]
rightVertexData =
[ PositionTextureVertex 0.0 1.0 0 0 0, -- Top-left (of right quad)
PositionTextureVertex 1.0 1.0 0 1 0, -- Top-right
PositionTextureVertex 1.0 (-1.0) 0 1 1, -- Bottom-right
PositionTextureVertex 0.0 (-1.0) 0 0 1 -- Bottom-left (of right quad)
]
-- Index Data (same for both quads)
indexData :: [Word16]
indexData = [0, 1, 2, 0, 2, 3]
-- AppResources
data AppResources = AppResources
{ resPipeline :: SDLGPUGraphicsPipeline,
resVertexBuffer :: SDLGPUBuffer, -- Common VB for drawing
resLeftVertexBuffer :: SDLGPUBuffer, -- Source VB for left side
resRightVertexBuffer :: SDLGPUBuffer, -- Source VB for right side
resIndexBuffer :: SDLGPUBuffer,
resTexture :: SDLGPUTexture, -- Common Texture for drawing
resLeftTexture :: SDLGPUTexture, -- Source Texture for left side
resRightTexture :: SDLGPUTexture, -- Source Texture for right side
resSampler :: SDLGPUSampler
}
deriving (Show)
-- main
main :: IO ()
main = do
sdlLog $ "Compiled SDL Version: " ++ show sdlVersion
linkedVersion <- sdlGetVersion
sdlLog $ "Linked SDL Version: " ++ show linkedVersion
maybeResult <- withContext "SDL3 Haskell GPU Copy Consistency" [] runAppGPU
case maybeResult of
Nothing -> sdlLog "Application initialization failed." >> exitFailure
Just _ -> sdlLog "Application finished successfully." >> exitSuccess
-- runAppGPU
runAppGPU :: Context -> IO ()
runAppGPU context = do
-- context is in scope here
sdlLog "Base context initialized."
bracket
(createResources context)
(releaseResources context)
$ \case
Nothing -> sdlLog "Failed to create resources. Exiting."
Just resources -> do
sdlLog "Resources created successfully."
eventLoopGPU context resources
-- createResources
createResources :: Context -> IO (Maybe AppResources)
createResources context@Context {..} = do
sdlLog "--- Beginning Resource Creation ---"
-- 1. Load Shaders (TexturedQuad.vert, TexturedQuad.frag)
let vertInfo = defaultShaderCreateInfo {shaderNumSamplers = 0} -- No samplers in vert shader
let fragInfo = defaultShaderCreateInfo {shaderNumSamplers = 1} -- One sampler in frag
maybeVertShader <- loadShader contextDevice "TexturedQuad.vert" SDL_GPU_SHADERSTAGE_VERTEX vertInfo
maybeFragShader <- loadShader contextDevice "TexturedQuad.frag" SDL_GPU_SHADERSTAGE_FRAGMENT fragInfo
-- 2. Load Images
maybeSurfL <- loadImage ("Content" </> "Images" </> "ravioli.bmp")
maybeSurfR <- loadImage ("Content" </> "Images" </> "ravioli_inverted.bmp")
-- 3. Create Graphics Pipeline
case (maybeVertShader, maybeFragShader, maybeSurfL, maybeSurfR) of
(Just vertS, Just fragS, Just surfL, Just surfR) ->
bracketOnError
(pure ((,) surfL surfR))
(\(sL, sR) -> sdlDestroySurface sL >> sdlDestroySurface sR)
$ \(sL, sR) -> do
surfLData <- peek sL
surfRData <- peek sR
-- Demo assumes images are same size, C uses assert
let imgW = surfaceW surfLData
let imgH = surfaceH surfLData
maybePipeline <- createDrawPipeline contextDevice contextWindow vertS fragS
sdlReleaseGPUShader contextDevice vertS
sdlReleaseGPUShader contextDevice fragS
-- 4. Create Textures
let texCI = (defaultTextureCreateInfo imgW imgH) {texInfoUsage = SDL_GPU_TEXTUREUSAGE_SAMPLER}
maybeTexL <- sdlCreateGPUTexture contextDevice texCI
maybeTexR <- sdlCreateGPUTexture contextDevice texCI
maybeTexActive <- sdlCreateGPUTexture contextDevice texCI -- Common active texture
-- 5. Create Sampler
let samplerCI = defaultSamplerCreateInfo SDL_GPU_FILTER_NEAREST
maybeSampler <- sdlCreateGPUSampler contextDevice samplerCI
-- 6. Create Buffers
(_, lvbSizeC, lvbSizeW32) <- calculateBufferDataSize leftVertexData "LeftVB"
maybeLVB <- createGPUBuffer contextDevice SDL_GPU_BUFFERUSAGE_VERTEX lvbSizeW32 "LeftVertexBuffer"
(_, rvbSizeC, rvbSizeW32) <- calculateBufferDataSize rightVertexData "RightVB"
maybeRVB <- createGPUBuffer contextDevice SDL_GPU_BUFFERUSAGE_VERTEX rvbSizeW32 "RightVertexBuffer"
-- Active VB size matches source VBs
maybeActiveVB <- createGPUBuffer contextDevice SDL_GPU_BUFFERUSAGE_VERTEX lvbSizeW32 "ActiveVertexBuffer"
(_, ibSizeC, ibSizeW32) <- calculateBufferDataSize indexData "Index"
maybeIB <- createGPUBuffer contextDevice SDL_GPU_BUFFERUSAGE_INDEX ibSizeW32 "IndexBuffer"
-- Upload initial data
uploadSuccess <- case (maybeTexL, maybeTexR, maybeLVB, maybeRVB, maybeIB) of
(Just texL, Just texR, Just lvb, Just rvb, Just ib) -> do
let totalVertexDataSize = lvbSizeW32 + rvbSizeW32
let totalTransferBufSize = totalVertexDataSize + ibSizeW32
let texLDataSize = fromIntegral (imgW * imgH * 4) :: Word32 -- Assuming 4bpp
let texRDataSize = texLDataSize
let totalTextureTransferSize = texLDataSize + texRDataSize
bracket
( (,)
<$> createTransferBuffer contextDevice totalTransferBufSize SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD "BufferTransfer"
<*> createTransferBuffer contextDevice totalTextureTransferSize SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD "TextureTransfer"
)
(\(mBuf, mTex) -> cleanupTransferBuffer contextDevice mBuf >> cleanupTransferBuffer contextDevice mTex)
$ \(mBufTrans, mTexTrans) ->
bracket
(sdlAcquireGPUCommandBuffer contextDevice)
cleanupCommandBuffer
$ \mCmd ->
case (mBufTrans, mTexTrans, mCmd) of
(Just bufTrans, Just texTrans, Just cmd) -> do
-- Map and poke buffer data
mapOkBuf <- bracket
(sdlMapGPUTransferBuffer contextDevice bufTrans False)
(\p -> when (isJust p) $ sdlUnmapGPUTransferBuffer contextDevice bufTrans)
$ \case
Nothing -> return False
Just ptr -> do
pokeArray (castPtr ptr) leftVertexData
pokeArray (castPtr ptr `plusPtr` fromIntegral lvbSizeW32) rightVertexData
pokeArray (castPtr ptr `plusPtr` fromIntegral totalVertexDataSize) indexData
return True
-- Map and poke texture data
mapOkTex <- bracket
(sdlMapGPUTransferBuffer contextDevice texTrans False)
(\p -> when (isJust p) $ sdlUnmapGPUTransferBuffer contextDevice texTrans)
$ \case
Nothing -> return False
Just ptr -> do
copyBytes ptr (surfacePixels surfLData) (fromIntegral texLDataSize)
copyBytes (ptr `plusPtr` fromIntegral texLDataSize) (surfacePixels surfRData) (fromIntegral texRDataSize)
return True
if mapOkBuf && mapOkTex
then do
mcp <- sdlBeginGPUCopyPass cmd
case mcp of
Nothing -> return False
Just cp -> do
sdlUploadToGPUBuffer cp (SDLGPUTransferBufferLocation bufTrans 0) (SDLGPUBufferRegion lvb 0 lvbSizeW32) False
sdlUploadToGPUBuffer cp (SDLGPUTransferBufferLocation bufTrans lvbSizeW32) (SDLGPUBufferRegion rvb 0 rvbSizeW32) False
sdlUploadToGPUBuffer cp (SDLGPUTransferBufferLocation bufTrans totalVertexDataSize) (SDLGPUBufferRegion ib 0 ibSizeW32) False
sdlUploadToGPUTexture cp (SDLGPUTextureTransferInfo texTrans 0 (fromIntegral imgW) (fromIntegral imgH)) (defaultTextureRegion texL imgW imgH) False
sdlUploadToGPUTexture cp (SDLGPUTextureTransferInfo texTrans texLDataSize (fromIntegral imgW) (fromIntegral imgH)) (defaultTextureRegion texR imgW imgH) False
sdlEndGPUCopyPass cp
sdlSubmitGPUCommandBuffer cmd >>= \s -> if s then sdlWaitForGPUIdle contextDevice >> return True else return False
else return False
_ -> return False
_ -> return False -- One of the initial texture/buffer creations failed
if uploadSuccess && isJust maybePipeline && isJust maybeTexActive && isJust maybeSampler && isJust maybeActiveVB && isJust maybeLVB && isJust maybeRVB && isJust maybeIB && isJust maybeTexL && isJust maybeTexR
then do
sdlLog "--- Resource Creation and Initial Upload Successful ---"
return $
Just
AppResources
{ resPipeline = fromJust maybePipeline,
resVertexBuffer = fromJust maybeActiveVB,
resLeftVertexBuffer = fromJust maybeLVB,
resRightVertexBuffer = fromJust maybeRVB,
resIndexBuffer = fromJust maybeIB,
resTexture = fromJust maybeTexActive,
resLeftTexture = fromJust maybeTexL,
resRightTexture = fromJust maybeTexR,
resSampler = fromJust maybeSampler
}
else do
sdlLog "!!! Failed to create one or more resources or upload failed."
-- Manual cleanup
maybe (pure ()) (sdlReleaseGPUGraphicsPipeline contextDevice) maybePipeline
maybe (pure ()) (sdlReleaseGPUTexture contextDevice) maybeTexL
maybe (pure ()) (sdlReleaseGPUTexture contextDevice) maybeTexR
maybe (pure ()) (sdlReleaseGPUTexture contextDevice) maybeTexActive
maybe (pure ()) (sdlReleaseGPUSampler contextDevice) maybeSampler
maybe (pure ()) (sdlReleaseGPUBuffer contextDevice) maybeLVB
maybe (pure ()) (sdlReleaseGPUBuffer contextDevice) maybeRVB
maybe (pure ()) (sdlReleaseGPUBuffer contextDevice) maybeActiveVB
maybe (pure ()) (sdlReleaseGPUBuffer contextDevice) maybeIB
return Nothing
_ -> sdlLog "Failed to load shaders or images." >> return Nothing
-- createDrawPipeline (Standard textured quad pipeline with alpha blend)
createDrawPipeline :: SDLGPUDevice -> SDLWindow -> SDLGPUShader -> SDLGPUShader -> IO (Maybe SDLGPUGraphicsPipeline)
createDrawPipeline dev win vertShader fragShader = do
swapchainFormat <- sdlGetGPUSwapchainTextureFormat dev win
let vertexSize = sizeOf (undefined :: PositionTextureVertex)
let texCoordOffset = sizeOf (undefined :: CFloat) * 3
let vertexAttributes =
[ SDLGPUVertexAttribute 0 0 SDL_GPU_VERTEXELEMENTFORMAT_FLOAT3 0,
SDLGPUVertexAttribute 1 0 SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2 (fromIntegral texCoordOffset)
]
vertexBufferDesc = [SDLGPUVertexBufferDescription 0 (fromIntegral vertexSize) SDL_GPU_VERTEXINPUTRATE_VERTEX 0]
vertexInputState = SDLGPUVertexInputState vertexBufferDesc vertexAttributes
-- Alpha Blend State from C example
let blendState =
defaultColorTargetBlendState
{ enableBlend = True,
alphaOp = SDL_GPU_BLENDOP_ADD,
blendOp = SDL_GPU_BLENDOP_ADD,
srcColorFactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA,
srcAlphaFactor = SDL_GPU_BLENDFACTOR_SRC_ALPHA,
dstColorFactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA,
dstAlphaFactor = SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA
}
let colorTargetDesc = defaultColorTargetDescription {targetFormat = swapchainFormat, targetBlendState = blendState}
targetInfo = SDLGPUGraphicsPipelineTargetInfo [colorTargetDesc] SDL_GPU_TEXTUREFORMAT_INVALID False
basePipelineCI = defaultGraphicsPipelineCreateInfo vertShader fragShader swapchainFormat
pipelineCI =
basePipelineCI
{ vertexInputState = vertexInputState,
targetInfo = targetInfo
}
sdlCreateGPUGraphicsPipeline dev pipelineCI
releaseResources :: Context -> Maybe AppResources -> IO ()
releaseResources _ Nothing = return ()
releaseResources Context {..} (Just AppResources {..}) = do
sdlLog "--> Releasing AppResources..."
sdlReleaseGPUGraphicsPipeline contextDevice resPipeline
sdlReleaseGPUBuffer contextDevice resVertexBuffer
sdlReleaseGPUBuffer contextDevice resLeftVertexBuffer
sdlReleaseGPUBuffer contextDevice resRightVertexBuffer
sdlReleaseGPUBuffer contextDevice resIndexBuffer
sdlReleaseGPUTexture contextDevice resTexture
sdlReleaseGPUTexture contextDevice resLeftTexture
sdlReleaseGPUTexture contextDevice resRightTexture
sdlReleaseGPUSampler contextDevice resSampler
sdlLog "<-- AppResources Released."
eventLoopGPU :: Context -> AppResources -> IO ()
eventLoopGPU context resources = do
sdlPumpEvents
shouldQuitRef <- newIORef False
processEventsGPU shouldQuitRef
shouldQuit <- readIORef shouldQuitRef
unless shouldQuit $ renderFrameGPU context resources >> eventLoopGPU context resources
processEventsGPU :: IORef Bool -> IO ()
processEventsGPU sr = sdlPollEvent >>= maybe (pure ()) (handleEventGPU >=> \q -> when q (writeIORef sr True) >> processEventsGPU sr)
handleEventGPU :: SDLEvent -> IO Bool
handleEventGPU (SDLEventQuit _) = sdlLog "Quit." >> return True
handleEventGPU (SDLEventKeyboard (SDLKeyboardEvent _ _ _ _ sc _ _ _ d _)) | d && sc == SDL_SCANCODE_Q = return True
handleEventGPU _ = return False
-- renderFrameGPU
renderFrameGPU :: Context -> AppResources -> IO ()
renderFrameGPU Context {..} AppResources {..} = do
maybeCmdbuf <- sdlAcquireGPUCommandBuffer contextDevice
case maybeCmdbuf of
Nothing -> sdlLog "Error: Failed to acquire render command buffer."
Just cmdbuf -> do
maybeSwapResult <- sdlWaitAndAcquireGPUSwapchainTexture cmdbuf contextWindow
case maybeSwapResult of
Nothing -> void (sdlSubmitGPUCommandBuffer cmdbuf `finally` pure ())
Just (swapchainTexture, _, _) -> do
let colorTargetInfoClear =
defaultColorTargetInfo
{ texture = swapchainTexture,
loadOp = SDL_GPU_LOADOP_CLEAR,
storeOp = SDL_GPU_STOREOP_STORE,
clearColor = SDLFColor 0 0 0 1
}
let colorTargetInfoLoad = colorTargetInfoClear {loadOp = SDL_GPU_LOADOP_LOAD}
let lvbSize = fromIntegral (sizeOf (head leftVertexData) * length leftVertexData) :: Word32
-- Common bindings for render pass
let bindCommonResources renderPass = do
sdlBindGPUGraphicsPipeline renderPass resPipeline
sdlBindGPUVertexBuffers renderPass 0 [SDLGPUBufferBinding resVertexBuffer 0]
sdlBindGPUIndexBuffer renderPass (SDLGPUBufferBinding resIndexBuffer 0) SDL_GPU_INDEXELEMENTSIZE_16BIT
sdlBindGPUFragmentSamplers renderPass 0 [SDLGPUTextureSamplerBinding resTexture resSampler]
-- Copy left-side resources & Draw
mCopyPassL <- sdlBeginGPUCopyPass cmdbuf
case mCopyPassL of
Nothing -> sdlLog "Failed to begin copy pass for left side."
Just cpL -> do
sdlCopyGPUBufferToBuffer cpL (SDLGPUBufferLocation resLeftVertexBuffer 0) (SDLGPUBufferLocation resVertexBuffer 0) lvbSize False
sdlCopyGPUTextureToTexture cpL (SDLGPUTextureLocation resLeftTexture 0 0 0 0 0) (SDLGPUTextureLocation resTexture 0 0 0 0 0) 16 16 1 False
sdlEndGPUCopyPass cpL
mRenderPassL <- sdlBeginGPURenderPass cmdbuf [colorTargetInfoClear] Nothing
case mRenderPassL of
Nothing -> sdlLog "Failed to begin render pass for left side."
Just rpL -> bindCommonResources rpL >> sdlDrawGPUIndexedPrimitives rpL 6 1 0 0 0 >> sdlEndGPURenderPass rpL
-- Copy right-side resources & Draw
mCopyPassR <- sdlBeginGPUCopyPass cmdbuf
case mCopyPassR of
Nothing -> sdlLog "Failed to begin copy pass for right side."
Just cpR -> do
sdlCopyGPUBufferToBuffer cpR (SDLGPUBufferLocation resRightVertexBuffer 0) (SDLGPUBufferLocation resVertexBuffer 0) lvbSize False -- Assuming RVB is same size
sdlCopyGPUTextureToTexture cpR (SDLGPUTextureLocation resRightTexture 0 0 0 0 0) (SDLGPUTextureLocation resTexture 0 0 0 0 0) 16 16 1 False
sdlEndGPUCopyPass cpR
mRenderPassR <- sdlBeginGPURenderPass cmdbuf [colorTargetInfoLoad] Nothing -- Load existing content
case mRenderPassR of
Nothing -> sdlLog "Failed to begin render pass for right side."
Just rpR -> bindCommonResources rpR >> sdlDrawGPUIndexedPrimitives rpR 6 1 0 0 0 >> sdlEndGPURenderPass rpR
submitted <- sdlSubmitGPUCommandBuffer cmdbuf
unless submitted $ sdlGetError >>= sdlLog . ("Submit failed: " ++)