--- a/src/mame/sharp/x68k.h	2026-08-04 16:54:53.872522860 -0500
+++ b/src/mame/sharp/x68k.h	2026-08-04 16:54:53.876522842 -0500
@@ -195,6 +195,12 @@
 	bool m_exp_nmi[2]{};
 	uint8_t m_current_ipl = 0;
 	int m_led_state = 0;
+	// Extra cycles charged per VRAM access, over and above a main-memory
+	// access, in 16.16 fixed point. Set from measured values in machine_start
+	// for the machines those measurements cover; 0 (unmodelled) otherwise.
+	uint32_t m_gvram_wait = 0;
+	uint32_t m_tvram_wait = 0;
+	uint32_t m_vram_wait_accum = 0;  // 16.16 carry between accesses
 	emu_timer* m_mouse_timer = nullptr;
 	emu_timer* m_led_timer = nullptr;
 	unsigned char m_scc_prev = 0;
@@ -265,6 +271,15 @@
 	uint16_t exp_r(offs_t offset, uint16_t mem_mask = ~0);
 	void exp_w(offs_t offset, uint16_t data, uint16_t mem_mask = ~0);
 
+	// VRAM wait states. The X68000's video RAM is slower than main memory;
+	// MAME charged nothing for it, so VRAM-bound code ran optimistically
+	// fast. These trampolines charge the difference and forward to the CRTC.
+	void charge_vram_wait(uint32_t increment);
+	uint16_t gvram_wait_r(offs_t offset);
+	void gvram_wait_w(offs_t offset, uint16_t data, uint16_t mem_mask = ~0);
+	uint16_t tvram_wait_r(offs_t offset);
+	void tvram_wait_w(offs_t offset, uint16_t data, uint16_t mem_mask = ~0);
+
 	uint16_t spritereg_r(offs_t offset);
 	void spritereg_w(offs_t offset, uint16_t data, uint16_t mem_mask = ~0);
 	uint16_t spriteram_r(offs_t offset);
--- a/src/mame/sharp/x68k.cpp	2026-08-04 16:54:53.871522864 -0500
+++ b/src/mame/sharp/x68k.cpp	2026-08-04 16:54:53.876522842 -0500
@@ -992,12 +992,80 @@
 	// Is this an undocumented MB89352 feature, an ASIC register, an original code bug or a bad dump?
 }
 
+// VRAM wait states.
+//
+// The 68000 in an X68000 does not reach video RAM as fast as it reaches main
+// memory, and MAME modelled no wait at all -- so any VRAM-bound inner loop ran
+// optimistically fast, which is exactly the code a graphics library cares
+// about. The figures used here are Makoto Kamada's real-hardware measurements
+// distributed with XEiJ (credited there to uchopon, tnb and ita), taken by
+// timing tst.w against each region on five 10 MHz X68000 variants:
+//
+//     region        tst.w time (us)   derived wait (cycles)
+//     main memory   1.642 .. 1.663    0.123
+//     GVRAM         1.734 .. 1.770    1.145
+//     TVRAM         1.829 .. 1.850    2.046
+//
+// MAME charges nothing for main memory, so what is added here is the wait
+// RELATIVE to main memory: 1.145 - 0.123 = 1.022 cycles for GVRAM and
+// 2.046 - 0.123 = 1.923 for TVRAM. Cross-check against the raw measurements:
+// GVRAM - main memory averages ~1.03 cycles and TVRAM - main memory ~1.92
+// across the five machines, which agrees.
+//
+// Main memory's own 0.123 cycles (DRAM refresh) is deliberately NOT modelled:
+// it would mean interposing on the hottest path in the driver to recover ~0.1
+// cycles per access, and it is a uniform offset that does not change the
+// relative cost of VRAM versus RAM, which is the thing being measured.
+//
+// The wait is fractional, so it accumulates in 16.16 fixed point and whole
+// cycles are charged as they carry out -- the same technique the apple2gs
+// driver uses for its 1 MHz cycles (see slow_cycle() there).
+#define X68K_GVRAM_WAIT 0x000105a2  // 1.022 cycles, 16.16
+#define X68K_TVRAM_WAIT 0x0001ec4a  // 1.923 cycles, 16.16
+
+void x68k_state::charge_vram_wait(uint32_t increment)
+{
+	// side_effects_disabled() covers debugger and Lua reads, which must not
+	// perturb the timing they are measuring.
+	if (increment == 0 || machine().side_effects_disabled())
+		return;
+	m_vram_wait_accum += increment;
+	int const cycles = m_vram_wait_accum >> 16;
+	m_vram_wait_accum &= 0xffff;
+	if (cycles != 0)
+		m_maincpu->adjust_icount(-cycles);
+}
+
+uint16_t x68k_state::gvram_wait_r(offs_t offset)
+{
+	charge_vram_wait(m_gvram_wait);
+	return m_crtc->gvram_r(offset);
+}
+
+void x68k_state::gvram_wait_w(offs_t offset, uint16_t data, uint16_t mem_mask)
+{
+	charge_vram_wait(m_gvram_wait);
+	m_crtc->gvram_w(offset, data, mem_mask);
+}
+
+uint16_t x68k_state::tvram_wait_r(offs_t offset)
+{
+	charge_vram_wait(m_tvram_wait);
+	return m_crtc->tvram_r(offset);
+}
+
+void x68k_state::tvram_wait_w(offs_t offset, uint16_t data, uint16_t mem_mask)
+{
+	charge_vram_wait(m_tvram_wait);
+	m_crtc->tvram_w(offset, data, mem_mask);
+}
+
 void x68k_state::x68k_base_map(address_map &map)
 {
 	map(0x000000, 0xbffffb).rw(FUNC(x68k_state::emptyram_r), FUNC(x68k_state::emptyram_w));
 	map(0xbffffc, 0xbfffff).rw(FUNC(x68k_state::rom0_r), FUNC(x68k_state::rom0_w));
-	map(0xc00000, 0xdfffff).rw(m_crtc, FUNC(x68k_crtc_device::gvram_r), FUNC(x68k_crtc_device::gvram_w));
-	map(0xe00000, 0xe7ffff).rw(m_crtc, FUNC(x68k_crtc_device::tvram_r), FUNC(x68k_crtc_device::tvram_w));
+	map(0xc00000, 0xdfffff).rw(FUNC(x68k_state::gvram_wait_r), FUNC(x68k_state::gvram_wait_w));
+	map(0xe00000, 0xe7ffff).rw(FUNC(x68k_state::tvram_wait_r), FUNC(x68k_state::tvram_wait_w));
 	map(0xe80000, 0xe81fff).rw(m_crtc, FUNC(x68k_crtc_device::crtc_r), FUNC(x68k_crtc_device::crtc_w));
 	map(0xe82400, 0xe83fff).rw(FUNC(x68k_state::vid_r), FUNC(x68k_state::vid_w));
 	map(0xe84000, 0xe85fff).rw(m_hd63450, FUNC(hd63450_device::read), FUNC(hd63450_device::write));
@@ -1155,6 +1223,24 @@
 	m_spriteram = (uint16_t*)(memregion("user1")->base());
 	space.install_ram(0x000000,m_ram->size()-1,m_ram->pointer());
 
+	// VRAM wait states, applied only to the machine the measurements cover.
+	// The published figures are for a 10 MHz 68000 X68000; XEiJ's table also
+	// lists a 25 MHz X68030 (GVRAM 6.377, TVRAM 6.623) but those machines are
+	// MACHINE_NOT_WORKING here and cannot be validated, so they are left
+	// unmodelled rather than given invented numbers.
+	if (m_maincpu->clock() == 10'000'000)
+	{
+		m_gvram_wait = X68K_GVRAM_WAIT;
+		m_tvram_wait = X68K_TVRAM_WAIT;
+	}
+	else
+	{
+		m_gvram_wait = 0;
+		m_tvram_wait = 0;
+	}
+	m_vram_wait_accum = 0;
+	save_item(NAME(m_vram_wait_accum));
+
 	// start mouse timer
 	m_mouse_timer->adjust(attotime::zero, 0, attotime::from_msec(1));  // a guess for now
 	m_mouse.inputtype = 0;
