diff --git a/Machines/Atari2600/TIA.cpp b/Machines/Atari2600/TIA.cpp
index 5e6acf1a2..0939ab090 100644
--- a/Machines/Atari2600/TIA.cpp
+++ b/Machines/Atari2600/TIA.cpp
@@ -272,6 +272,8 @@ void TIA::set_playfield_control_and_ball_size(uint8_t value)
 			playfield_priority_ = PlayfieldPriority::OnTop;
 		break;
 	}
+
+	ball_.size = 1 << ((value >> 4)&3);
 }
 
 void TIA::set_playfield_ball_colour(uint8_t colour)
@@ -308,6 +310,7 @@ void TIA::set_player_graphic(int player, uint8_t value)
 {
 	player_[player].graphic[1] = value;
 	player_[player^1].graphic[0] = player_[player^1].graphic[1];
+	if(player) ball_.enabled[0] = ball_.enabled[1];
 }
 
 void TIA::set_player_reflected(int player, bool reflected)
@@ -358,18 +361,25 @@ void TIA::set_missile_motion(int missile, uint8_t motion)
 
 void TIA::set_ball_enable(bool enabled)
 {
+	ball_.enabled[1] = enabled;
 }
 
 void TIA::set_ball_delay(bool delay)
 {
+	ball_.enabled_index = delay ? 0 : 1;
 }
 
 void TIA::set_ball_position()
 {
+	position_[(int)MotionIndex::Ball] = 0;
+
+	// setting the ball position also triggers a draw
+	ball_.pixel_position = ball_.size;
 }
 
 void TIA::set_ball_motion(uint8_t motion)
 {
+	motion_[(int)MotionIndex::Ball] = (motion >> 4) & 0xf;
 }
 
 void TIA::move()
@@ -763,8 +773,93 @@ void TIA::draw_missile_visible(Missile &missile, CollisionType collision_identit
 
 void TIA::draw_ball(int start, int end)
 {
+	int first_pixel = first_pixel_cycle - 4 + (horizontal_blank_extend_ ? 8 : 0);
+
+	// movement works across the entire screen, so do work that falls outside of the pixel area
+	if(start < first_pixel)
+	{
+		ball_.pixel_position = std::max(0, ball_.pixel_position - perform_border_motion((int)MotionIndex::Ball, start, std::max(end, first_pixel)));
+	}
+
+	// don't continue to do any drawing if this window ends too early
+	if(end < first_pixel) return;
+	if(start < first_pixel) start = first_pixel;
+	if(start >= end) return;
+
+	// perform the visible part of the line, if any
+	if(start < 224)
+	{
+		draw_ball_visible(start - first_pixel_cycle + 4, std::min(end - first_pixel_cycle + 4, 160));
+	}
+
+	// move further if required
+	if(is_moving_[(int)MotionIndex::Ball] && end >= 224 && motion_time_[(int)MotionIndex::Ball] < end)
+	{
+		perform_motion_step((int)MotionIndex::Ball);
+		ball_.pixel_position = std::max(0, ball_.pixel_position - 1);
+	}
 }
 
 void TIA::draw_ball_visible(int start, int end)
 {
+	int &position = position_[(int)MotionIndex::Ball];
+
+	// perform a miniature event loop on (i) triggering draws; (ii) drawing; and (iii) motion
+	int next_motion_time = motion_time_[(int)MotionIndex::Ball] - first_pixel_cycle + 4;
+	while(start < end)
+	{
+		int next_event_time = end;
+
+		// is the next event a movement tick?
+		if(is_moving_[(int)MotionIndex::Ball] && next_motion_time < next_event_time)
+		{
+			next_event_time = next_motion_time;
+		}
+
+		// is the next event a graphics trigger?
+		if(ball_.enabled[ball_.enabled_index])
+		{
+			int time_until_copy = 160 - position;
+			int next_copy_time = start + time_until_copy;
+			if(next_copy_time < next_event_time) next_event_time = next_copy_time;
+		}
+
+		// the decision is to progress by length
+		const int length = next_event_time - start;
+
+		if(ball_.pixel_position)
+		{
+			int output_cursor = 0;
+			if(ball_.enabled[ball_.enabled_index])
+			{
+				while(ball_.pixel_position && output_cursor < length)
+				{
+					collision_buffer_[start + output_cursor] |= (uint8_t)CollisionType::Ball;
+					output_cursor++;
+					ball_.pixel_position--;
+				}
+			}
+			else
+			{
+				ball_.pixel_position = std::max(0, ball_.pixel_position - length);
+			}
+		}
+
+		// the next interesting event is after next_event_time cycles, so progress
+		position = (position + length) % 160;
+		start = next_event_time;
+
+		// if the event is a motion tick, apply
+		if(is_moving_[(int)MotionIndex::Ball] && start == next_motion_time)
+		{
+			perform_motion_step((int)MotionIndex::Ball);
+			next_motion_time += 4;
+		}
+
+		// if it's a draw trigger, trigger a draw
+		if(!position)
+		{
+			ball_.pixel_position = ball_.size;
+		}
+	}
 }
diff --git a/Machines/Atari2600/TIA.hpp b/Machines/Atari2600/TIA.hpp
index 77e52729d..2bb5af4bc 100644
--- a/Machines/Atari2600/TIA.hpp
+++ b/Machines/Atari2600/TIA.hpp
@@ -153,6 +153,17 @@ class TIA {
 			int size;		// 0 = 1 pixel, 1 = 2 pixels, etc
 		} missile_[2];
 
+		// ball state
+		struct Ball {
+			bool enabled[2];
+			int enabled_index;
+			int size;
+
+			int pixel_position;
+
+			Ball() : pixel_position(0), size(1), enabled_index(0) {}
+		} ball_;
+
 		// movement
 		bool horizontal_blank_extend_;
 		int motion_[5];