diff --git a/Manifest.toml b/Manifest.toml
index cb947fc7d597428061bd6555a34cd6150c0c3161..be1ca3fafc7e2a39a580d9000435a12e4a9136bc 100644
--- a/Manifest.toml
+++ b/Manifest.toml
@@ -2,7 +2,7 @@
 
 julia_version = "1.9.3"
 manifest_format = "2.0"
-project_hash = "5a2d131ebba9918c9648a958a8d008d9dd7bc967"
+project_hash = "bcd13ee640573e4651fc680662e595f5fd73216d"
 
 [[deps.AbstractFFTs]]
 deps = ["LinearAlgebra"]
@@ -231,9 +231,9 @@ version = "0.5.0"
 
 [[deps.ChainRulesCore]]
 deps = ["Compat", "LinearAlgebra", "SparseArrays"]
-git-tree-sha1 = "e30f2f4e20f7f186dc36529910beaedc60cfa644"
+git-tree-sha1 = "b66b8f8e3db5d7835fb8cbe2589ffd1cd456e491"
 uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
-version = "1.16.0"
+version = "1.17.0"
 
 [[deps.CircularArrays]]
 deps = ["OffsetArrays"]
@@ -243,9 +243,9 @@ version = "1.3.2"
 
 [[deps.CodecZlib]]
 deps = ["TranscodingStreams", "Zlib_jll"]
-git-tree-sha1 = "02aa26a4cf76381be7f66e020a3eddeb27b0a092"
+git-tree-sha1 = "cd67fc487743b2f0fd4380d4cbd3a24660d0eec8"
 uuid = "944b1d66-785c-5afd-91f1-9de20f533193"
-version = "0.7.2"
+version = "0.7.3"
 
 [[deps.ColorBrewer]]
 deps = ["Colors", "JSON", "Test"]
@@ -512,9 +512,9 @@ uuid = "2e619515-83b5-522b-bb60-26c02a35a201"
 version = "2.5.0+0"
 
 [[deps.Extents]]
-git-tree-sha1 = "5e1e4c53fa39afe63a7d356e30452249365fba99"
+git-tree-sha1 = "2140cd04483da90b2da7f99b2add0750504fc39c"
 uuid = "411431e0-e8b7-467b-b5e0-f676ba4f2910"
-version = "0.1.1"
+version = "0.1.2"
 
 [[deps.FFMPEG]]
 deps = ["FFMPEG_jll"]
@@ -1363,9 +1363,9 @@ version = "10.42.0+0"
 
 [[deps.PDMats]]
 deps = ["LinearAlgebra", "SparseArrays", "SuiteSparse"]
-git-tree-sha1 = "fcf8fd477bd7f33cb8dbb1243653fb0d415c256c"
+git-tree-sha1 = "3d8af7d689bd228a3a6c2298a4e6d5f5944accb8"
 uuid = "90014a1f-27ba-587c-ab20-58faa44d9150"
-version = "0.11.25"
+version = "0.11.26"
 
 [[deps.PNGFiles]]
 deps = ["Base64", "CEnum", "ImageCore", "IndirectArrays", "OffsetArrays", "libpng_jll"]
@@ -1411,7 +1411,7 @@ version = "2.7.2"
 
 [[deps.Persefone]]
 deps = ["Agents", "ArgParse", "CSV", "CairoMakie", "DataFrames", "DataFramesMeta", "Dates", "Distributed", "FileIO", "GeoArrays", "ImageMagick", "Logging", "LoggingExtras", "Pkg", "Random", "Serialization", "StableRNGs", "StatsBase", "TOML", "Test"]
-git-tree-sha1 = "febeb873c49f62cc4c320f5461d0aef90823621d"
+git-tree-sha1 = "b24521422c9b1a1099a8e9d1c4db06c4fdd7050a"
 repo-rev = "development"
 repo-url = "../model"
 uuid = "039acd1d-2a07-4b33-b082-83a1ff0fd136"
@@ -1487,7 +1487,9 @@ version = "1.9.0"
 
 [[deps.QML]]
 deps = ["ColorTypes", "CxxWrap", "Libdl", "MacroTools", "Observables", "Qt6Wayland_jll", "Requires", "jlqml_jll"]
-git-tree-sha1 = "8b4065aab80821daae07602de0293fa8f9a59222"
+git-tree-sha1 = "676fd7e228e5895c3a45e3d5fea3a619133c3af8"
+repo-rev = "main"
+repo-url = "https://github.com/JuliaGraphics/QML.jl.git"
 uuid = "2db162a6-7e43-52c3-8d84-290c1c42d82a"
 version = "0.8.0"
 
@@ -1603,6 +1605,12 @@ git-tree-sha1 = "838a3a4188e2ded87a4f9f184b4b0d78a1e91cb7"
 uuid = "ae029012-a4dd-5104-9daa-d747884805df"
 version = "1.3.0"
 
+[[deps.ResumableFunctions]]
+deps = ["MacroTools"]
+git-tree-sha1 = "d0399c12a584c18ed77a351925cd07f5a9b32c36"
+uuid = "c5292f4c-5179-55e1-98c5-05642aab7184"
+version = "0.6.6"
+
 [[deps.Rmath]]
 deps = ["Random", "Rmath_jll"]
 git-tree-sha1 = "f65dcb5fa46aee0cf9ed6274ccbd597adc49aa7b"
@@ -1939,9 +1947,9 @@ uuid = "9d95f2ec-7b3d-5a63-8d20-e2491e220bb9"
 version = "1.4.3"
 
 [[deps.URIs]]
-git-tree-sha1 = "b7a5e99f24892b6824a954199a45e9ffcc1c70f0"
+git-tree-sha1 = "67db6cc7b3821e19ebe75791a9dd19c9b1188f2b"
 uuid = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
-version = "1.5.0"
+version = "1.5.1"
 
 [[deps.UUIDs]]
 deps = ["Random", "SHA"]
@@ -2189,9 +2197,9 @@ version = "0.2.3+0"
 
 [[deps.jlqml_jll]]
 deps = ["Artifacts", "JLLWrappers", "Libdl", "Qt6Declarative_jll", "Qt6Svg_jll", "libcxxwrap_julia_jll"]
-git-tree-sha1 = "a27c3b21a4f0fc3f659fb4efdc6147877cbe5d4c"
+git-tree-sha1 = "d4a59a1816b53c6db845acb04ed8f74c0a3aa9de"
 uuid = "6b5019fb-a83d-5b4e-a9f7-678a36c28df7"
-version = "0.5.3+0"
+version = "0.5.4+0"
 
 [[deps.libaom_jll]]
 deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
diff --git a/Project.toml b/Project.toml
index 78a3fcd702b19285fda855c4664414ff351e4ede..bedbfecce453f485343a7eff4dbf31647572d048 100644
--- a/Project.toml
+++ b/Project.toml
@@ -15,4 +15,10 @@ Observables = "510215fc-4207-5dde-b226-833fc4488ee2"
 Persefone = "039acd1d-2a07-4b33-b082-83a1ff0fd136"
 QML = "2db162a6-7e43-52c3-8d84-290c1c42d82a"
 Qt6Base_jll = "c0090381-4147-56d7-9ebc-da0b1113ec56"
+ResumableFunctions = "c5292f4c-5179-55e1-98c5-05642aab7184"
 TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
+TableTransforms = "0d432bfd-3ee1-4ac1-886a-39f05cc69a3e"
+
+[compat]
+GLMakie = "0.6"
+Makie = "0.17"
diff --git a/src/GUI.jl b/src/GUI.jl
index e0c4c35e8c3f3a7beb70aca868a9b45286031cee..d0f4c6e790aa5940ea301ab89d6fcf671bc04b95 100644
--- a/src/GUI.jl
+++ b/src/GUI.jl
@@ -8,10 +8,15 @@
 ## - https://github.com/barche/QmlJuliaExamples
 ## - https://doc.qt.io/qt-6/qtquick-index.html
 
+#TODO define standard config file for Persefone Desktop
+
 global model = nothing
 global landcovermap = nothing
-#loadmodelobject(joinpath(dirname(@__FILE__), "../sampleinit.dat")) #initialise()
+global runsimulation = nothing
+global timer = nothing
+
 const running = Observable(false)
+const ticks = Observable(0)
 const date = Observable(today())
 const progress = Observable(0.0)
 const delay = Observable(0.5)
@@ -33,7 +38,12 @@ end
 
 function nextstep()
     global model
-    if date[] < model.date
+    if date[] >= @param(core.enddate)
+        running[] = false
+        runbuttontext[] = ">>"
+        runbuttontip[] = "Run"
+        return
+    elseif date[] < model.date
         # If we've "scrolled back" in time, step forward again without
         # having to resimulate (see `previousstep()`)
         date[] += Day(1)
@@ -45,6 +55,7 @@ function nextstep()
     end
     progress[] = (date[]-@param(core.startdate)) /
         (@param(core.enddate)-@param(core.startdate))
+    @emit updateMakie()
     println("Updated model: $(date[]-Day(1))")
 end
 
@@ -53,58 +64,64 @@ function previousstep()
     # "scroll back" in time and visualise the state of the model at
     # a previous date
     global model
+    date[] <= @param(core.startdate) && return
     date[] -= Day(1)
     progress[] = (date[]-@param(core.startdate)) /
         (@param(core.enddate)-@param(core.startdate))
+    @emit updateMakie()
     println("Backtracked model: $(date[]-Day(1))")
 end
-    
-function runsimulation()
-    #XXX is this all I need to interrupt a run?
+
+@resumable function createrunfunction()
     while running[]
         nextstep()
-        sleep(delay)
+        #XXX to reduce interface lag, always only sleep for 0.1s?
+        sleep(delay[])
+        @yield 1
     end
 end
 
 function togglerunning()
+    global runsimulation, timer
     if running[]
         running[] = false
+        QML.stop(timer)
         runbuttontext[] = ">>"
         runbuttontip[] = "Run"
     else
         running[] = true
+        QML.start(timer)
         runbuttontext[] = "||"
         runbuttontip[] = "Pause"
-        runsimulation()
+        runsimulation = createrunfunction()
     end
 end
 
 function render_map(screen)
     global model, landcovermap
+    println("Updating map")
     figure = visualisemap(model, date[], landcovermap)
     display(screen, figure.scene)
 end
 
 function render_plot(screen)
     global model
+    println("Updating plot")
     figure = populationtrends(model)
     display(screen, figure.scene)
 end
 
-@qmlfunction newsimulation
-@qmlfunction nextstep
-@qmlfunction previousstep
-@qmlfunction togglerunning
-@qmlfunction render_map
-@qmlfunction render_plot
-
 on(delay) do d
-    println("Delay is now $(round(d, digits=1)) seconds.")
+    println("Delay is now $(round(d, digits=1)) seconds.") #XXX DEBUG
 end
 
 on(running) do r
-    r ? println("Simulation started.") : println("Simulation stopped.")
+    ticks = 0 #prevent overflows
+    r ? println("Simulation started.") : println("Simulation stopped.") #XXX DEBUG
+end
+
+on(ticks) do t
+    running[] && runsimulation()
 end
 
 function splashscreen()
@@ -114,7 +131,7 @@ function splashscreen()
     QML.show(quick_view)
     #XXX exec_async() is currently still broken, but should be fixed soon
     # (https://github.com/JuliaGraphics/QML.jl/issues/174)
-    exec_async()
+    @async exec()
 end
 
 """
@@ -123,18 +140,30 @@ end
 The main function that creates the application.
 """
 function launch()
-    global model
-    splashscreen()
+    global model, timer
+    #splashscreen()
     newsimulation()
-    # absolute path in case working dir is overridden
+    
+    @qmlfunction newsimulation
+    @qmlfunction nextstep
+    @qmlfunction previousstep
+    @qmlfunction togglerunning
+
+    timer = QTimer()
+
     qmlfile = joinpath(dirname(@__FILE__), "main.qml")
     loadqml(qmlfile,
+            timer = timer,
             vars = JuliaPropertyMap("running" => running,
+                                    "ticks" => ticks,
                                     "date" => date,
                                     "delay" => delay,
                                     "progress" => progress,
                                     "runbuttontext" => runbuttontext,
-                                    "runbuttontip" => runbuttontip))
+                                    "runbuttontip" => runbuttontip),
+            render_map_callback = @safe_cfunction(render_map, Cvoid, (Any,)),
+            render_plot_callback = @safe_cfunction(render_plot, Cvoid, (Any,)))
+    @emit updateMakie()
     println("Launched Persefone Desktop.")
     exec()
 end
diff --git a/src/PersefoneDesktop.jl b/src/PersefoneDesktop.jl
index 3dc670bef8394bf92e54527182feb8990ae54d2c..4d9372911c2e14a3483db4cb85fd8566a41fa7d9 100644
--- a/src/PersefoneDesktop.jl
+++ b/src/PersefoneDesktop.jl
@@ -22,7 +22,8 @@ using
     CxxWrap,
     GLMakie,
     Persefone,
-    Observables
+    Observables,
+    ResumableFunctions
 
 # needed for Makie integration
 ENV["QSG_RENDER_LOOP"] = "basic"
diff --git a/src/main.qml b/src/main.qml
index fbaf1aff9c27eddb73ebb60437a23033bde46d7f..a95a4ee1c1284c394c0fe86876dc87fdef0160a6 100644
--- a/src/main.qml
+++ b/src/main.qml
@@ -3,17 +3,18 @@
 /// This file defines the layout of the main window.
 ///
 
+import org.julialang
 import QtQuick
 import QtQuick.Controls
 import QtQuick.Dialogs
 import QtQuick.Layouts
-import org.julialang
 
 ApplicationWindow {
 	id: mainWindow
 	title: "Persefone.jl Desktop"
 	width: 1024
 	height: 768
+	visibility: "Maximized"
 	visible: true
 	
 	menuBar: MenuBar {
@@ -23,7 +24,10 @@ ApplicationWindow {
 				text: "&New Simulation"
 				onTriggered: { Julia.newsimulation() } //TODO select config file
 			}
-			Action { text: "&Configure Simulation" }
+			Action {
+				text: "&Configure Simulation"
+				onTriggered: { SetupWindow.show() } //FIXME
+			}
 			Action { text: "&Load Saved State" }
 			Action { text: "&Save Current State" }
 			Action { text: "Save Model &Output" }
@@ -60,15 +64,16 @@ Distributed under the MIT license."
 	}
 
 	RowLayout {
+		spacing: 6
 		anchors.fill: parent
 
 		// visualise the model map and the locations of animals
-		/* MakieViewport { */
-		/* 	id: mapviewport */
-		/* 	Layout.fillWidth: true */
-		/* 	Layout.fillHeight: true */
-		/* 	renderFunction: Julia.render_map() //TODO */
-		/* } */
+		MakieViewport {
+			id: mapviewport
+			Layout.fillWidth: true
+			Layout.fillHeight: true
+			renderFunction: render_map_callback
+		}
 
 		ColumnLayout {
 
@@ -104,12 +109,12 @@ Distributed under the MIT license."
 			}
 			
 			// plot the population sizes over time
-			/* MakieViewport { */
-			/* 	id: plotviewport */
-			/* 	Layout.fillWidth: true */
-			/* 	Layout.fillHeight: true */
-			/* 	renderFunction: Julia.render_plot() //TODO */
-			/* } */
+			MakieViewport {
+				id: plotviewport
+				Layout.fillWidth: true
+				Layout.fillHeight: true
+				renderFunction: render_plot_callback //TODO
+			}
 		}
 	}
 
@@ -148,8 +153,6 @@ Distributed under the MIT license."
 			}
 			ProgressBar {
 				id: progressBar
-				from: 0
-				to: 100
 				value: vars.progress
 				Layout.fillWidth: true
 				ToolTip.text: "Simulation progress"
@@ -168,4 +171,19 @@ Distributed under the MIT license."
 			}
 		}
 	}
+
+	// set up connections and signals to update the simulation and the display
+	Connections {
+		target: timer
+		function onTimeout() { vars.ticks += 1 }
+	}
+	
+	JuliaSignals {
+		signal updateMakie()
+		onUpdateMakie: {
+			mapviewport.update();
+			plotviewport.update();
+		}
+	}
+
 }