diff --git a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme
index f9049689f..782b17c09 100644
--- a/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme	
+++ b/OSBindings/Mac/Clock Signal.xcodeproj/xcshareddata/xcschemes/Clock Signal.xcscheme	
@@ -68,7 +68,7 @@
       </AdditionalOptions>
    </TestAction>
    <LaunchAction
-      buildConfiguration = "Debug"
+      buildConfiguration = "Release"
       selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
       enableASanStackUseAfterReturn = "YES"
diff --git a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift
index 930c53470..2161613e1 100644
--- a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift	
+++ b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift	
@@ -198,17 +198,21 @@ class MachineDocument:
 	}
 
 	// MARK: CSOpenGLViewDelegate
-	final func openGLView(_ view: CSOpenGLView, drawViewOnlyIfDirty onlyIfDirty: Bool) {
-		bestEffortLock.lock()
-		if let bestEffortUpdater = bestEffortUpdater {
-			bestEffortLock.unlock()
-			bestEffortUpdater.update()
-			if drawLock.try() {
-				self.machine.drawView(forPixelSize: view.backingSize, onlyIfDirty: onlyIfDirty)
-				drawLock.unlock()
-			}
-		} else {
-			bestEffortLock.unlock()
+	final func openGLViewRedraw(_ view: CSOpenGLView, event redrawEvent: CSOpenGLViewRedrawEvent) {
+		switch redrawEvent {
+			case .timer:
+				bestEffortLock.lock()
+				if let bestEffortUpdater = bestEffortUpdater {
+					bestEffortLock.unlock()
+					bestEffortUpdater.update()
+				} else {
+					bestEffortLock.unlock()
+				}
+				self.machine.updateView(forPixelSize: view.backingSize)
+				fallthrough
+
+			case .appKit:
+				self.machine.drawView(forPixelSize: view.backingSize)
 		}
 	}
 
diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.h b/OSBindings/Mac/Clock Signal/Machine/CSMachine.h
index dcfa52da2..ca824bbc3 100644
--- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.h	
+++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.h	
@@ -54,7 +54,9 @@ typedef NS_ENUM(NSInteger, CSMachineKeyboardInputMode) {
 - (void)setAudioSamplingRate:(float)samplingRate bufferSize:(NSUInteger)bufferSize;
 
 - (void)setView:(nullable CSOpenGLView *)view aspectRatio:(float)aspectRatio;
-- (void)drawViewForPixelSize:(CGSize)pixelSize onlyIfDirty:(BOOL)onlyIfDirty;
+
+- (void)updateViewForPixelSize:(CGSize)pixelSize;
+- (void)drawViewForPixelSize:(CGSize)pixelSize;
 
 - (void)setKey:(uint16_t)key characters:(nullable NSString *)characters isPressed:(BOOL)isPressed;
 - (void)clearAllKeys;
diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm
index 62e94753f..c43535b04 100644
--- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm	
+++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm	
@@ -240,8 +240,12 @@ struct ActivityObserver: public Activity::Observer {
 	_machine->crt_machine()->set_scan_target(_scanTarget.get());
 }
 
-- (void)drawViewForPixelSize:(CGSize)pixelSize onlyIfDirty:(BOOL)onlyIfDirty {
-	_scanTarget->draw(true, (int)pixelSize.width, (int)pixelSize.height);
+- (void)updateViewForPixelSize:(CGSize)pixelSize {
+	_scanTarget->update((int)pixelSize.width, (int)pixelSize.height);
+}
+
+- (void)drawViewForPixelSize:(CGSize)pixelSize {
+	_scanTarget->draw((int)pixelSize.width, (int)pixelSize.height);
 }
 
 - (void)paste:(NSString *)paste {
diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h
index 6d6fcd42a..0fd718a27 100644
--- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h	
+++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h	
@@ -11,15 +11,24 @@
 
 @class CSOpenGLView;
 
+typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) {
+	/// Indicates that AppKit requested a redraw for some reason (mostly likely, the window is being resized). So,
+	/// if the delegate doesn't redraw the view, the user is likely to see a graphical flaw.
+	CSOpenGLViewRedrawEventAppKit,
+	/// Indicates that the view's display-linked timer has triggered a redraw request. So, if the delegate doesn't
+	/// redraw the view, the user will just see the previous drawing without interruption.
+	CSOpenGLViewRedrawEventTimer
+};
+
 @protocol CSOpenGLViewDelegate
 /*!
 	Requests that the delegate produce an image of its current output state. May be called on
 	any queue or thread.
 	@param view The view making the request.
-	@param onlyIfDirty If @c YES then the delegate may decline to redraw if its output would be
+	@param redrawEvent If @c YES then the delegate may decline to redraw if its output would be
 	identical to the previous frame. If @c NO then the delegate must draw.
 */
-- (void)openGLView:(nonnull CSOpenGLView *)view drawViewOnlyIfDirty:(BOOL)onlyIfDirty;
+- (void)openGLViewRedraw:(nonnull CSOpenGLView *)view event:(CSOpenGLViewRedrawEvent)redrawEvent;
 
 /*!
 	Announces receipt of a file by drag and drop to the delegate.
diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m
index a458b2054..c00a2b15d 100644
--- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m	
+++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m	
@@ -53,7 +53,15 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
 - (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency
 {
 	// Draw the display now regardless of other activity.
-	[self drawViewOnlyIfDirty:YES];
+	[self performWithGLContext:^{
+		[self.delegate openGLViewRedraw:self event:CSOpenGLViewRedrawEventTimer];
+		CGLFlushDrawable([[self openGLContext] CGLContextObj]);
+	}];
+}
+
+- (void)drawRect:(NSRect)dirtyRect
+{
+	[self.delegate openGLViewRedraw:self event:CSOpenGLViewRedrawEventAppKit];
 }
 
 - (void)invalidate
@@ -119,19 +127,6 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
 	[self registerForDraggedTypes:@[(__bridge NSString *)kUTTypeFileURL]];
 }
 
-- (void)drawRect:(NSRect)dirtyRect
-{
-	[self drawViewOnlyIfDirty:NO];
-}
-
-- (void)drawViewOnlyIfDirty:(BOOL)onlyIfDirty
-{
-	[self performWithGLContext:^{
-		[self.delegate openGLView:self drawViewOnlyIfDirty:onlyIfDirty];
-		CGLFlushDrawable([[self openGLContext] CGLContextObj]);
-	}];
-}
-
 - (void)performWithGLContext:(dispatch_block_t)action
 {
 	CGLLockContext([[self openGLContext] CGLContextObj]);
diff --git a/OSBindings/SDL/main.cpp b/OSBindings/SDL/main.cpp
index d0833e937..d030be004 100644
--- a/OSBindings/SDL/main.cpp
+++ b/OSBindings/SDL/main.cpp
@@ -776,7 +776,8 @@ int main(int argc, char *argv[]) {
 
 		// Display a new frame and wait for vsync.
 		updater.update();
-		scan_target.draw(true, int(window_width), int(window_height));
+		scan_target.update(int(window_width), int(window_height));
+		scan_target.draw(int(window_width), int(window_height));
 		if(activity_observer) activity_observer->draw();
 		SDL_GL_SwapWindow(window);
 	}
diff --git a/Outputs/OpenGL/ScanTarget.cpp b/Outputs/OpenGL/ScanTarget.cpp
index 2c8ed5b8f..a37408f63 100644
--- a/Outputs/OpenGL/ScanTarget.cpp
+++ b/Outputs/OpenGL/ScanTarget.cpp
@@ -383,10 +383,10 @@ void ScanTarget::setup_pipeline() {
 	input_shader_->set_uniform("textureName", GLint(SourceDataTextureUnit - GL_TEXTURE0));
 }
 
-void ScanTarget::draw(bool synchronous, int output_width, int output_height) {
+void ScanTarget::update(int output_width, int output_height) {
 	if(fence_ != nullptr) {
 		// if the GPU is still busy, don't wait; we'll catch it next time
-		if(glClientWaitSync(fence_, GL_SYNC_FLUSH_COMMANDS_BIT, synchronous ? GL_TIMEOUT_IGNORED : 0) == GL_TIMEOUT_EXPIRED) {
+		if(glClientWaitSync(fence_, GL_SYNC_FLUSH_COMMANDS_BIT, 0) == GL_TIMEOUT_EXPIRED) {
 			return;
 		}
 		fence_ = nullptr;
@@ -666,15 +666,6 @@ void ScanTarget::draw(bool synchronous, int output_width, int output_height) {
 		test_gl(glDisable, GL_BLEND);
 	}
 
-	// Copy the accumulatiion texture to the target.
-	test_gl(glBindFramebuffer, GL_FRAMEBUFFER, target_framebuffer_);
-	test_gl(glViewport, 0, 0, (GLsizei)output_width, (GLsizei)output_height);
-
-	test_gl(glClearColor, 0.0f, 0.0f, 0.0f, 0.0f);
-	test_gl(glClear, GL_COLOR_BUFFER_BIT);
-	accumulation_texture_->bind_texture();
-	accumulation_texture_->draw(float(output_width) / float(output_height), 4.0f / 255.0f);
-
 	// All data now having been spooled to the GPU, update the read pointers to
 	// the submit pointer location.
 	read_pointers_.store(submit_pointers);
@@ -684,3 +675,18 @@ void ScanTarget::draw(bool synchronous, int output_width, int output_height) {
 	fence_ = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0);
 	is_drawing_.clear();
 }
+
+void ScanTarget::draw(int output_width, int output_height) {
+	while(is_drawing_.test_and_set());
+
+	// Copy the accumulation texture to the target.
+	test_gl(glBindFramebuffer, GL_FRAMEBUFFER, target_framebuffer_);
+	test_gl(glViewport, 0, 0, (GLsizei)output_width, (GLsizei)output_height);
+
+	test_gl(glClearColor, 0.0f, 0.0f, 0.0f, 0.0f);
+	test_gl(glClear, GL_COLOR_BUFFER_BIT);
+	accumulation_texture_->bind_texture();
+	accumulation_texture_->draw(float(output_width) / float(output_height), 4.0f / 255.0f);
+
+	is_drawing_.clear();
+}
diff --git a/Outputs/OpenGL/ScanTarget.hpp b/Outputs/OpenGL/ScanTarget.hpp
index 8a870d957..1f2e001a5 100644
--- a/Outputs/OpenGL/ScanTarget.hpp
+++ b/Outputs/OpenGL/ScanTarget.hpp
@@ -42,7 +42,10 @@ class ScanTarget: public Outputs::Display::ScanTarget {
 
 		void set_target_framebuffer(GLuint);
 
-		void draw(bool synchronous, int output_width, int output_height);
+		/*! Pushes the current state of output to the target framebuffer. */
+		void draw(int output_width, int output_height);
+		/*! Processes all the latest input, at a resolution suitable for later output to a framebuffer of the specified size. */
+		void update(int output_width, int output_height);
 
 	private:
 #ifndef NDEBUG