blitz-foundation / monkey2

zlib License
3 stars 0 forks source link

Not a bug: upload to graphics card in separate thread? #94

Open Pharmhaus-2 opened 5 years ago

Pharmhaus-2 commented 5 years ago

Original Author: DruggedBunny

Hi Mark,

This is just something I thought a) you might like to see, and b) something that might be suitable for a later to-do.

I ported a cool terrain generator, then made it multithreaded, just out of interest, to try and reduce the pauses when generating a new terrain. Weirdly, it worked first try! (Latest code is below, in this 'issue'.)

This takes about a second to do the terrain creation (Model.CreateTerrain), in a background thread, but once the thread completes, there's a shorter jerk of around 300-400 ms.

My best guess would be that this is where the main thread uploads the data to the graphics card, and so I looked it up, and it seems it might be possible to upload the data on a separate thread.

Is it possible to implement this, or maybe have Async versions of slow operations like this (eg. model loaders), so that your Model reference only gets assigned once the upload's complete?

Code's all in one lump, but jump to OnRender -> If Keyboard.KeyHit (Key.G) and the thread function GenerateTerrain_Threaded ().

PlayfulJSTerrain generates a Pixmap-based heightmap, then GenerateTerrain (called by GenerateTerrain_Threaded) turns it into a Model.

Thought it might be something of interest to look at longer term, anyway. Could help with possible streaming of bigger worlds, etc.

#Import "<std>"
#Import "<mojo>"
#Import "<mojo3d>"
#Import "<thread>"

Using std..
Using mojo..
Using mojo3d..

#Rem

    Basic usage to generate 2D PNG heightmap (size must be power of 2):

            Local terrain:PlayfulJSTerrain = New PlayfulJSTerrain (seed, pixmap_size, roughness) ' eg. 1, 1024, 0.5

            Local heightmap:Pixmap = terrain.RenderPixmap ()

            heightmap.Save (DesktopDir () + "\mx2__rendered__heightmap.png")

#End

' -----------------------------------------------------------------------------
' Start of PlayfulJSTerrain
' -----------------------------------------------------------------------------

Class PlayfulJSTerrain

    ' Hunter Loftis
    ' http://www.playfuljs.com/realistic-terrain-in-130-lines/
    ' https://github.com/hunterloftis/playfuljs-demos/blob/gh-pages/terrain/index.html

    Field size:Int
    Field max:Int
    Field map:Float[]
    Field roughness:Float

    Field upper:Float
    Field lower:Float

    Method New (seed:ULong, pixmap_size:Int = 1024, in_roughness:Float = 0.5)

        Assert (IsPow2 (pixmap_size), "PlayfulJSTerrain.New (pixmap_size) must be power of 2!")

        SeedRnd (seed)

        roughness = in_roughness
        size = pixmap_size + 1       'Pow (2, Log2 (pixmap_size)) + 1
        max = size - 1

        map = New Float[size * size]

'       Local ticks:Int = Millisecs ()
        Generate ()
'       Print Millisecs () - ticks

        ' About 15 ms on AMD FX6350 @ 4.2 GHz

    End

    Method GetHeight:Float (x:Int, y:Int)
        If (x < 0 Or x > max Or y < 0 Or y > max) Return -1
        Return map[x + size * y]
    End

    Method SetHeight (x:Int, y:Int, val:Float)
        map[x + size * y] = val
        If val < lower Then lower = val
        If val > upper Then upper = val
    End

    Method Generate ()

        SetHeight (0, 0, max)
        SetHeight (max, 0, max * 0.5)
        SetHeight (max, max, 0)
        SetHeight (0, max, max * 0.5)

        Divide (max)

    End

    Method Divide (size:Int)

        Local half:Int = size / 2

        If half < 1 Then Return

        Local scale:Float = roughness * size
        Local scale2:Float = scale * 2 ' Quick pre-calc for the two loops below...

        Local x:Int
        Local y:Int

        For y = half To max Step size
            For x = half To max Step size
                Square (x, y, half, Rnd () * scale2 - scale)
            Next
        Next

        For y = 0 To max Step half
            For x = (y + half) Mod size To max Step size
                Diamond (x, y, half, Rnd () * scale2 - scale)
            Next
        Next

        ' Recursive call until too small (see "If half < 1")...

        Divide (size / 2)

    End

    Method Square (x:Int, y:Int, size:Int, offset:Float)

        ' Pre-calc gains ~ 0.5 to 1 ms!

        Local xms:Int = x - size
        Local yms:Int = y - size

        Local xps:Int = x + size
        Local yps:Int = y + size

        Local average:Float = ( GetHeight (xms, yms) + GetHeight (xps, yms) +
                                GetHeight (xps, yps) + GetHeight (xms, yps)) * 0.25

        SetHeight (x, y, average + offset)

    End

    Method Diamond (x:Int, y:Int, size:Int, offset:Float)

        ' No pre-calc, nothing repeated...

        Local average:Float = ( GetHeight (x, y - size) + GetHeight (x + size, y) + 
                                GetHeight (x, y + size) + GetHeight (x - size, y)) * 0.25

        SetHeight (x, y, average + offset)

    End

    Method RenderPixmap:Pixmap ()

        Local pix:Pixmap = New Pixmap (max, max, PixelFormat.I8)

        Local rgb:Float
        Local color:Color = New Color (0.0, 0.0, 0.0)

'       Local ticks:Int = Millisecs ()

        For Local y:Int = 0 Until max
            For Local x:Int = 0 Until max

                If (upper - lower) = 0 ' Make sure range is valid!
                    rgb = 0.5 ' TODO: Not sure what this should be...
                Else
                    rgb = TransformRange (GetHeight (x, y), lower, upper, 0.0, 1.0)
                Endif

                ' Let's not keep New'ing in heavy loop...

                color.R = rgb
                color.G = rgb
                color.B = rgb

                pix.SetPixel (x, y, color)

            Next
        Next

'       Print Millisecs () - ticks ' Around 20 ms

        Return pix

    End

    Method TransformRange:Float (input_value:Float, from_min:Float, from_max:Float, to_min:Float, to_max:Float)

        ' Algorithm via jerryjvl at https://stackoverflow.com/questions/929103/convert-a-number-range-to-another-range-maintaining-ratio

        Local from_delta:Float  = from_max  - from_min  ' Input range,  eg. 0.0 - 1.0
        Local to_delta:Float    = to_max    - to_min    ' Output range, eg. 5.0 - 10.0

        Assert (from_delta <> 0.0, "TransformRange: Invalid input range!")

        Return (((input_value - from_min) * to_delta) / from_delta) + to_min

    End

    Method IsPow2:Long (value:Long)
        Return Not (value & (value - 1))
    End

End

' -----------------------------------------------------------------------------
' End of PlayfulJSTerrain
' -----------------------------------------------------------------------------

' Helper function to generate checkerboard Pixmap...

Function CheckerPixmap:Pixmap (color0:Color, color1:Color)

    Local pixels:Pixmap = New Pixmap (256, 256, PixelFormat.RGBA8)
    pixels.Clear (color0)

    Local pixel_toggle:Int = 0

    For Local gp_y:Int = 0 Until pixels.Height
        For Local gp_x:Int = 0 Until pixels.Width
            If pixel_toggle Then pixels.SetPixel (gp_x, gp_y, color1)
            pixel_toggle = 1 - pixel_toggle
        Next
        pixel_toggle = 1 - pixel_toggle
    Next

    Return pixels

End

' Demo...

Class MyWindow Extends Window

    Field scene:Scene
    Field camera:Camera
    Field light:Light
    Field seed:Int = 0
    Field turn:Bool = True

    Field ground:Model

    Field ground_thread:Thread

    Field render_timer:Int

    Method New( title:String="PlayfulJSTerrain to Monkey2!",width:Int=1024,height:Int=768,flags:WindowFlags=WindowFlags.Center )
'   Method New( title:String="PlayfulJSTerrain to Monkey2!",width:Int=App.DesktopSize.X,height:Int=App.DesktopSize.Y,flags:WindowFlags=WindowFlags.Fullscreen )
        Super.New( title,width,height,flags )
    End

    Method GenerateTerrain_Threaded ()
        Local ticks:Int = Millisecs ()
        Local tg:Model = GenerateTerrain (seed, 1024)
        ground?.Destroy ()
        ground = tg
        Print "Terrain rendering thread took " + (Millisecs () - ticks) + " ms"
        ' About a second...
    End

    Method OnCreateWindow() Override

        scene=New Scene
        scene.ClearColor = Color.Sky
        scene.FogColor = Color.Sky
        scene.FogNear = 256
        scene.FogFar = 1024

        camera=New Camera( Self )
        camera.AddComponent<FlyBehaviour>()
        camera.Move( 0,300,0 )
        camera.Far = 1024.0
        camera.Rotate (45, 0, 0)

        light=New Light
        light.CastsShadow=True
        light.Move (-512, 256, 0)

        ' GenerateTerrain returns a mojo3d Model...

        'ground = GenerateTerrain (seed, 1024)

        ground_thread = New Thread (GenerateTerrain_Threaded)

        Mouse.PointerVisible = False

        render_timer = Millisecs ()

    End

    Method OnRender( canvas:Canvas ) Override

        Local render_time:Int = Millisecs () - render_timer

        render_timer = Millisecs ()

        If render_time > 17
            Print "Rendering exceeded 17 ms, taking " + render_time + " ms"
        Endif

        If turn Then ground?.Rotate (0.0, 0.125, 0.0)

        If Keyboard.KeyHit (Key.Escape) Then App.Terminate ()

        If Keyboard.KeyHit (Key.T) Then turn = Not turn ' Toggle turn variable on/off...

        If Keyboard.KeyHit (Key.G)

            ' One at a time, please...

            If Not ground_thread.Running

                seed = seed + 1

                ground_thread = New Thread (GenerateTerrain_Threaded)

            Endif

        Endif

        ' Hitting S demonstrates simple non-3D usage to generate a heightmap...

        If Keyboard.KeyHit (Key.S)

            ' Create a terrain with same details as 3D one...

            Local terrain:PlayfulJSTerrain = New PlayfulJSTerrain (seed) ' Defaults to size = 1024 x 1024, roughness = 0.5

            ' Render to Pixmap and save to desktop...

            Local heightmap:Pixmap = terrain.RenderPixmap ()
            heightmap.Save (DesktopDir () + "\mx2__rendered__heightmap.png")

        Endif

        RequestRender()
        scene.Update()
        camera.Render( canvas )

        canvas.DrawText( "Seed=" + seed + ", FPS="+App.FPS,20,20 )
        canvas.DrawText( "G to generate new terrain",20,40 )
        canvas.DrawText( "S to save heightmap to desktop",20,60 )
        canvas.DrawText( "T to toggle rotation",20,80 )
        canvas.DrawText( "Cursors + A/Z to move (slowly!)",20,100 )

        canvas.DrawText( "Esc to exit",20,140 )

        If ground_thread.Running
            canvas.DrawText( "Generating new terrain...",20,180 )
        Endif

    End

End

' For demo only...

Function GenerateTerrain:Model (seed:ULong, size:Int = 1024, roughness:Float = 0.5, color0:Color = New Color (0.8, 0.5, 0.1), color1:Color = New Color (0.75, 0.35, 0.05))

    Local terrain:PlayfulJSTerrain = New PlayfulJSTerrain (seed, size, roughness)

        Local heightmap:Pixmap = terrain.RenderPixmap ()

        If Not heightmap Then RuntimeError ("Failed to generate heightmap!")

        heightmap.FlipY () ' 2D Y (increases downwards) translates to 3D Z (increases upwards)

    Local terrain_material:PbrMaterial = New PbrMaterial ()

        terrain_material.ColorTexture = New Texture (CheckerPixmap (color0, color1), TextureFlags.None)

    Local terrain_height:Float = 256.0 ' TODO!

    Local height_box:Boxf = New Boxf (-heightmap.Width * 0.5, 0.0, -heightmap.Height * 0.5, heightmap.Width * 0.5, terrain_height, heightmap.Height * 0.5)

    Return Model.CreateTerrain (heightmap, height_box, New PbrMaterial (terrain_material))

End

Function Main()

    New AppInstance

    New MyWindow

    App.Run()

End