You haven’t truly met “graphics” until you’ve watched a cursor drag itself across the screen like it’s hauling a sofa uphill.
In the pre-NVIDIA world, the bottleneck wasn’t some exotic shader bug. It was you, a CPU, a memory bus, and a chunk of RAM
that happened to be mapped to a screen.
This is the era where a “graphics subsystem” could mean: a CRT controller, a palette, a bitplane layout you’d better not misunderstand,
and a driver that might be doing more improvisational art than rendering. If you operate production systems today, you’ll recognize the patterns:
hidden bandwidth limits, deceptive metrics, and “optimizations” that move the problem from one choke point to another.
What “graphics” meant before GPUs
Today “graphics” implies a pipeline: shaders, textures, command buffers, VRAM, and a driver stack that’s half kernel, half black magic.
Before NVIDIA made “GPU” a household word, “graphics” was mostly about getting pixels into a frame buffer fast enough that humans
didn’t notice you were faking motion.
Most systems lived in one of three worlds:
-
Text mode pretending to be graphics. Character cells, a font ROM, and maybe some block characters for “UI.”
Cheap, reliable, and fast because it moved bytes, not pixels. -
2D bitmaps with minimal acceleration. A frame buffer in VRAM; drawing meant copying rectangles, drawing lines,
and filling regions. If you had a blitter, you were living large. -
3D as a specialist tax. Workstations with expensive add-on boards or proprietary pipelines. On consumer PCs,
“3D” often meant software rendering: clever math, fixed-point tricks, and acceptance.
The common thread: moving memory. “Rendering” wasn’t a philosophical debate. It was “how many bytes can I shovel per frame”
and “how many times do I touch each pixel.”
If you want the core mental model, it’s this: pre-GPU graphics is IO.
Your screen is a continuously read memory device. Your job is to update that memory without tripping over the bus, the CPU cache,
or the display’s refresh timing.
Concrete facts and historical context
A few short, sharp context points that matter because they explain why software was written the way it was:
APIs, file formats, UI toolkits, and performance folklore didn’t come from nowhere.
-
IBM’s VGA (1987) standardized 640×480 with 16 colors and a 256-color 320×200 mode.
That 320×200×8bpp mode became a default game canvas because it was a sweet spot for speed and memory footprint. -
“Mode 13h” (320×200, 256 colors) on DOS mapped VRAM linearly.
Linear addressing meant a CPU could write pixels with simple pointer arithmetic—no planar gymnastics required. -
Planar modes were common and painful.
In 16-color VGA modes, pixels lived across bitplanes. A single pixel write could mean read-modify-write across planes.
That shaped everything from sprite engines to why certain fonts looked the way they did. -
VESA BIOS Extensions (VBE) made higher resolutions possible on DOS.
It wasn’t “plug and play”; it was “if you’re lucky and your card’s firmware behaves.” -
Early 2D acceleration was about rectangles, not triangles.
BitBLT (bit block transfer) engines, hardware line draw, and hardware cursors mattered more than anything “3D.” -
The bus was destiny: ISA vs VLB vs PCI.
ISA bandwidth and latency limited frame buffer writes; VLB and PCI made higher-throughput graphics practical by feeding VRAM faster. -
Color palettes were a performance feature.
8-bit indexed color meant a full screen buffer could be ~64 KB (320×200) or ~300 KB (640×480), small enough for the era.
Palette cycling enabled “animation” without touching most pixels. -
3D accelerators initially lived as fixed-function helpers.
Early consumer 3D cards accelerated texture mapping and triangle setup; the CPU still did a lot, especially game logic and transforms. -
Double-buffering wasn’t “free.”
Having two full frame buffers meant doubling VRAM use and increasing copy bandwidth. Many systems used dirty rectangles instead.
The old pipeline: from CPU to phosphor
1) The frame buffer wasn’t a metaphor
A modern GPU driver stack queues work; the GPU pulls it and writes to VRAM with massive internal parallelism.
In the older world, the CPU was the renderer. If you wanted a pixel lit, you wrote to the address that represented that pixel.
The display controller continuously scanned VRAM and generated the video signal.
That arrangement created a simple but unforgiving truth: graphics performance was memory performance.
Not just “fast RAM,” but where the RAM lived and how expensive it was to reach.
VRAM behind a slow bus is like an S3 bucket accessed through a dial-up modem: technically correct, practically cruel.
2) What “acceleration” meant
Pre-GPU acceleration usually meant a chip that could:
- copy a rectangle from one area of VRAM to another (BitBLT)
- fill a rectangle with a solid color
- draw lines
- support a hardware cursor overlay (so the mouse didn’t tear or lag)
This was huge. If you can copy rectangles in hardware, you can scroll windows, move sprites, and redraw UI elements
without eating the CPU alive. It’s the same reason modern systems love DMA: copy engines free compute for actual work.
3) Why people obsessed over pixel formats
Pixel formats were not aesthetic; they were survival. If you pick 8bpp indexed color, you reduce memory footprint and bandwidth.
If you pick 16bpp (often 5-6-5 RGB), you increase bandwidth but simplify shading and avoid palette tricks. If you pick 24bpp,
you’re asking your bus to do cardio.
And formats weren’t always linear. Planar layouts, banked frame buffers (where you switch which chunk of VRAM is visible at an address),
and alignment constraints meant that a naive “just loop over pixels” could be catastrophically slow.
4) Tearing and the tyranny of refresh
The display controller reads VRAM at a fixed cadence. If you write to VRAM while it’s scanning out, you can see half-updated frames:
tearing. The “right” fix is synchronization—waiting for vertical blanking or using page flipping. The expensive fix is copying.
The cheap fix is “don’t redraw too much and hope nobody notices.”
Systems that got this right felt magically smooth. Systems that didn’t felt like your eyes were debugging.
Joke #1: In those days, “real-time rendering” meant “rendering in time for the next meeting.” Occasionally it made both.
Where the time went: bottlenecks you could feel
Bandwidth: the silent budget
If you want a rule of thumb for old-school graphics, compute your raw write budget.
A 640×480 screen at 8bpp is ~300 KB. At 60 fps that’s ~18 MB/s just to write one full frame—ignoring reads,
ignoring overdraw, ignoring blits, ignoring everything else your CPU and bus are doing.
On paper that sounds modest today. In context, it was frequently the whole machine.
Old buses, caches, and memory controllers could turn “18 MB/s” into a fantasy once you add contention and wait states.
This is why partial redraw (dirty rectangles) wasn’t a micro-optimization; it was the difference between usable and insulting.
Latency: why a single pixel could be expensive
VRAM behind a bus can have ugly access patterns. Sequential writes might be okay; scattered pixel writes can become a horror show.
You see this in software sprite engines: they batch, they align, they prefer spans. They do anything to turn random writes into linear ones.
Overdraw: painting the same pixel multiple times
In a software renderer, every time you touch a pixel you pay. If your UI toolkit repaints the entire window for a blinking caret,
you don’t have a “UI bug.” You have a throughput problem. Same in games: if you draw background, then draw sprites, then redraw UI,
the same pixel might get written three times.
The correct instinct in that era was: minimize touches. Paint once. Cache aggressively. Clip ruthlessly.
Sometimes the best rendering technique was “don’t.”
CPU cycles: math vs memory
The old rendering debates (“use fixed-point,” “precompute tables,” “avoid divisions”) weren’t academic.
CPUs were slow enough that math could dominate. But often the opposite was true: the CPU could compute faster than it could write
to the frame buffer. That’s when you see tricks like drawing into a system-memory buffer and copying in larger blocks.
It seems wasteful—until you remember caches exist and buses don’t forgive.
Drivers: the thin layer between you and pain
If you were on DOS, you talked to BIOS calls or hit registers directly. If you were on early Windows or X11,
you trusted a driver model that might have been written by a vendor whose main KPI was “boots most of the time.”
If you were in workstations (SGI, Sun, HP), you often had better integration—at workstation prices.
In production terms: the reliability of your graphics was a supply chain problem.
Your “rendering correctness” depended on firmware, bus timings, and chipset quirks.
The reliability angle: graphics as an operational dependency
People forget how operationally critical “graphics” was in corporate environments:
trading floors, CAD stations, medical imaging, kiosk systems, call centers. If the UI lagged, the business lagged.
If the screen went black after a suspend/resume, the help desk got a new hobby.
A pre-GPU graphics stack is an SRE parable: performance cliffs, partial failures, non-obvious limits,
and complicated interactions between hardware and software. You don’t “tune it once.” You keep it stable.
One quote, because it applies to every era of systems and every screen that ever stuttered:
Hope is not a strategy.
—paraphrased idea often attributed in operations circles; treat it as a reminder, not a citation.
Three corporate mini-stories from the trenches
Mini-story 1: The incident caused by a wrong assumption (color depth as “just a setting”)
An internal logistics dashboard was deployed across a few hundred thin clients and refurbished desktops.
It was mostly 2D: charts, grids, and a map widget. The rollout went fine in the pilot—newer machines, decent PCI graphics,
plenty of RAM. Then it hit the warehouse floor.
The symptom was weirdly human: workers complained the UI “felt sticky.” Mouse moves were delayed, scrolling lagged,
and after a few minutes the app would “catch up” in bursts. It didn’t crash. It just aged in place.
Support tried the usual: reinstall, reboot, swap mice, blame the network. Nothing consistent.
The wrong assumption was that switching from 16-bit to 32-bit color was a harmless cosmetic improvement.
The new build defaulted to 32bpp because it looked cleaner on modern monitors and avoided banding on gradients.
On the older machines, the frame buffer writes doubled, and the driver fell back from accelerated 2D paths to a slow software path
for some operations in 32bpp.
The kicker: the app redrew large regions on every timer tick—fine in 16bpp on the pilot hardware, disastrous in 32bpp on ISA-era leftovers.
You could watch CPU usage stay moderate while the bus choked; the UI thread wasn’t pegged, it was blocked on slow writes and driver calls.
The fix wasn’t heroic. They locked the fleet to 16bpp on those endpoints, patched the app to reduce redraw regions,
and added a startup check that refused to enable “fancy gradients” unless a small benchmark passed.
The lesson: “settings” are part of the performance contract. Treat them like a schema change.
Mini-story 2: The optimization that backfired (double buffering everywhere)
A kiosk product team wanted smoother animations: no tearing, no flicker. They did what every blog post says:
implement double buffering. Render into an off-screen buffer, then blit to the screen. Clean. Predictable. Modern.
They shipped it, proud of the new polish.
Two weeks later, the field reports came in: random stalls, occasional black frames, and a slow drift into lag after several hours.
The devices had modest RAM and integrated graphics sharing memory bandwidth with the CPU. The kiosks also did background work:
local logging, occasional uploads, and image decoding.
Double buffering turned small incremental updates into full-frame copies. The old code used dirty rectangles;
it would redraw only what changed. The new code always rendered the full scene (even static background) into the back buffer
and then copied the whole thing to the front. Bandwidth skyrocketed. Cache pressure increased. And when memory got tight,
paging started nibbling at the edges.
Worse: their “blit to screen” call didn’t always use hardware acceleration on some chipsets. In the lab it was fast;
in the field it sometimes fell back to a slower path, especially in certain color depths. The optimization was correct in theory,
and wrong in deployment.
They backed out the universal double buffer and implemented a hybrid:
double buffer only for the animated region, keep dirty rectangles for the rest, and add a watchdog that detects when blits
get slower and degrades animation gracefully. They also pinned memory usage to avoid swapping.
The lesson: a smoother frame is worthless if you can’t sustain it. Don’t optimize for aesthetics without measuring the bus.
Mini-story 3: The boring but correct practice that saved the day (baseline capture + rollback)
A finance department ran a legacy X11 application on Linux thin clients for years. It wasn’t pretty, but it was stable.
The team responsible for endpoints had one habit that looked absurdly conservative: before any driver update, they captured a baseline
of hardware IDs, kernel modules, Xorg logs, and basic 2D performance metrics. Then they stored it with the change request.
A vendor pushed a “security update” that included a graphics stack refresh. After deployment to a subset, users reported that window moves
left trails and text redraw lagged. Not everyone—only certain models. The help desk started collecting screenshots. Engineers started guessing.
That’s how time dies.
The endpoint team compared the new logs against their baseline. They immediately saw that acceleration was disabled on affected units:
the driver module changed, but those clients had a slightly different PCI ID revision. The new stack treated them as unsupported and fell back
to a generic framebuffer driver.
Because they had baselines, they didn’t need a war room. They rolled back the graphics package for those models, pinned versions,
and opened a vendor ticket with precise evidence: module mismatch, fallback path, and reproducible steps.
The lesson: boring instrumentation beats heroic debugging. Baselines aren’t paperwork; they’re time travel.
Joke #2: Nothing boosts team morale like discovering your “performance regression” is actually “we turned off acceleration.” It’s like fixing a leak by turning the water back on.
Practical tasks: commands, outputs, and what the output means
These tasks assume you’re diagnosing a legacy-ish graphics path on Linux: framebuffer, X11, or basic DRM/KMS.
Even if you’re not running retro hardware, the same checks expose the classic failure modes: fallback drivers, disabled acceleration,
bandwidth starvation, and redraw storms.
Task 1: Identify the GPU / graphics controller
cr0x@server:~$ lspci -nn | egrep -i 'vga|3d|display'
00:02.0 VGA compatible controller [0300]: Intel Corporation 82865G Integrated Graphics Controller [8086:2572] (rev 02)
Meaning: You’re not guessing anymore; you have vendor and device IDs.
Decision: Look up which driver should bind (i915, nouveau, mga, etc.). If you see an ancient integrated controller,
assume shared memory bandwidth and fragile acceleration paths.
Task 2: See which kernel driver actually bound to the device
cr0x@server:~$ lspci -k -s 00:02.0
00:02.0 VGA compatible controller: Intel Corporation 82865G Integrated Graphics Controller (rev 02)
Subsystem: Dell Device 0163
Kernel driver in use: i915
Kernel modules: i915
Meaning: “Kernel driver in use” is the truth. “Kernel modules” is what could be used.
Decision: If it says vesafb or fbdev when you expect a native DRM driver, you’re likely in a slow path.
Task 3: Confirm DRM/KMS state and detect fallback to simpledrm
cr0x@server:~$ dmesg | egrep -i 'drm|fb0|simpledrm|vesafb' | tail -n 12
[ 1.234567] simpledrm: framebuffer at 0xe0000000, 0x300000 bytes
[ 1.234890] simpledrm: format=a8r8g8b8, mode=1024x768x32, linelength=4096
[ 2.101010] [drm] Initialized i915 1.6.0 20201103 for 0000:00:02.0 on minor 0
Meaning: Early boot might start with simpledrm and later switch to a real driver. That’s normal.
Decision: If you never see the real driver initialize, you’re stuck on generic framebuffer. Expect poor 2D performance.
Task 4: Check Xorg for acceleration being disabled
cr0x@server:~$ grep -E "(EE|WW|Accel|glamor|uxa|sna)" /var/log/Xorg.0.log | tail -n 20
[ 22.123] (II) modeset(0): glamor X acceleration enabled on Mesa DRI Intel(R) 865G
[ 22.125] (WW) modeset(0): Disabling glamor because of old hardware
[ 22.126] (II) modeset(0): Using shadow framebuffer
Meaning: “shadow framebuffer” is code for “software will do extra copies.”
Decision: On old hardware this might be unavoidable; then you reduce redraw and pixel depth. On newer hardware, it’s a misconfiguration.
Task 5: Check which OpenGL renderer you got (hardware vs software)
cr0x@server:~$ glxinfo -B | egrep 'OpenGL vendor|OpenGL renderer|OpenGL version'
OpenGL vendor string: Mesa
OpenGL renderer string: llvmpipe (LLVM 15.0.7, 256 bits)
OpenGL version string: 4.5 (Compatibility Profile) Mesa 23.0.4
Meaning: llvmpipe means software rendering. Your CPU is the GPU now.
Decision: If you expected hardware acceleration, stop tuning the app and fix the driver stack. If you accept software rendering,
constrain resolution/effects and budget CPU accordingly.
Task 6: See whether direct rendering is enabled
cr0x@server:~$ glxinfo | grep -i "direct rendering"
direct rendering: Yes
Meaning: “Yes” suggests DRI is working, but it doesn’t guarantee performance (could still be software).
Decision: Pair it with the renderer string. If direct rendering is “No,” expect severe slowness and tearing.
Task 7: Confirm the current mode (resolution + refresh)
cr0x@server:~$ xrandr --current
Screen 0: minimum 320 x 200, current 1024 x 768, maximum 8192 x 8192
VGA-1 connected primary 1024x768+0+0 (normal left inverted right x axis y axis) 0mm x 0mm
1024x768 60.00*+
800x600 60.32
640x480 59.94
Meaning: You’ve got 1024×768@60. That’s the raw pixel budget you must redraw.
Decision: If performance is bad, drop resolution or color depth first. This is the fastest lever with the biggest effect.
Task 8: Check whether the kernel is reporting GPU hangs or resets
cr0x@server:~$ dmesg | egrep -i 'hang|reset|gpu|ring|fault' | tail -n 20
[ 912.332100] i915 0000:00:02.0: GPU HANG: ecode 9:1:0x85dffffb, in Xorg [1234]
[ 913.001234] i915 0000:00:02.0: Resetting chip for hang on rcs0
Meaning: You’re not dealing with “slow”; you’re dealing with instability that causes stalls.
Decision: Reduce acceleration features, try an older/newer driver, or change the workload (disable compositing).
Don’t paper over this with app-side sleeps.
Task 9: Measure CPU saturation and context switching during UI lag
cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 512000 42000 320000 0 0 1 2 180 300 25 10 65 0 0
5 1 0 98000 41000 250000 0 0 0 120 900 3200 65 20 10 5 0
4 2 20480 12000 20000 120000 20 40 10 600 1200 5000 70 15 5 10 0
Meaning: Rising r, low id, and non-zero si/so indicates contention and swapping.
Decision: If swap starts, fix memory pressure first (lower resolution, disable compositing, reduce caches, or add RAM).
Swapping turns “graphics” into “distributed systems,” and not in a fun way.
Task 10: Identify memory bandwidth pressure (quick proxy via perf)
cr0x@server:~$ sudo perf stat -a -e cycles,instructions,cache-misses,context-switches -d -- sleep 5
Performance counter stats for 'system wide':
12,345,678,901 cycles
6,789,012,345 instructions # 0.55 insn per cycle
234,567,890 cache-misses
12,345 context-switches
5.001234567 seconds time elapsed
Meaning: Low IPC plus high cache misses during stutter is consistent with memory-bound rendering or copy-heavy paths.
Decision: Reduce pixel touching (dirty rectangles), reduce color depth, and avoid per-pixel alpha blends that force reads.
Task 11: Check the compositor status (a frequent hidden tax)
cr0x@server:~$ ps -ef | egrep -i 'picom|compton|mutter|kwin_x11' | grep -v grep
cr0x 1888 1 5 10:01 ? 00:02:11 picom --config /home/cr0x/.config/picom.conf
Meaning: A compositor can turn simple 2D copies into blended textures and extra buffering.
Decision: On weak/old GPUs or software rendering, disable compositing. If you need it, tune it (vsync off/on, shadows off).
Task 12: Capture a lightweight X11 rendering benchmark (sanity check)
cr0x@server:~$ x11perf -copywinwin500 | head -n 20
x11perf: description: local server
x11perf: drawing to window 500x500
CopyWinWin500 200 reps @ 4.50 msec ( 444.4/sec): Copy 500x500 from window to window
Meaning: This approximates how fast the system can copy a moderate rectangle—exactly what old-school 2D UIs do.
Decision: Compare across machines or before/after changes. If it’s an order of magnitude slower after an update, suspect driver fallback.
Task 13: Check framebuffer device details (when you’re truly in fbdev land)
cr0x@server:~$ cat /sys/class/graphics/fb0/virtual_size
1024,768
Meaning: Confirms the framebuffer’s virtual size; useful when apps assume one mode but the kernel chose another.
Decision: If this doesn’t match your expected mode, fix KMS mode setting or Xorg config before touching the app.
Task 14: Check kernel module list for conflicting framebuffers
cr0x@server:~$ lsmod | egrep 'i915|nouveau|radeon|amdgpu|vesafb|simpledrm|fbdev' | head
i915 3121152 3
drm_kms_helper 315392 1 i915
drm 622592 4 drm_kms_helper,i915
simpledrm 20480 1
Meaning: Multiple framebuffer drivers can coexist, but conflicts can pin you to a generic driver.
Decision: If vesafb is loaded alongside a real DRM driver and you see issues, blacklist the generic one cautiously and test.
Task 15: Detect “redraw storms” from an app (high X11 traffic)
cr0x@server:~$ xrestop -b | head -n 12
res-base win pix GC col fnt pmap other total PID Login Name
36 12 820 18 12 2 0 45 945 2444 cr0x legacy-dashboard
22 8 120 10 6 1 0 18 177 1888 cr0x picom
Meaning: Growing pixmaps and resources can indicate excessive offscreen buffering or leaks that degrade performance over time.
Decision: If an app’s pixmaps climb indefinitely, treat it like a memory leak; restart policy or patch is justified.
Task 16: Check disk IO and journaling pressure (because swapping and logging happen)
cr0x@server:~$ iostat -xz 1 3
avg-cpu: %user %nice %system %iowait %steal %idle
35.21 0.00 12.10 9.88 0.00 42.81
Device r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await %util
sda 2.00 45.00 64.00 980.00 45.16 0.90 19.50 92.00
Meaning: High %util and await indicates the disk is busy; if the UI lags, you might be paging or log-bound.
Decision: Stop the IO bleed: reduce logging verbosity, move logs, add RAM, or fix the disk. Graphics doesn’t exist in isolation.
Fast diagnosis playbook
When a system “feels slow,” the fastest way to waste time is to argue about feelings.
Run the following in order and you’ll usually identify the bottleneck class within 10 minutes.
First: confirm you’re not on a fallback driver
- Run
lspci -kand verify the expected kernel driver is in use. - Check
dmesgforsimpledrm/vesafbonly, with no proper DRM driver initialization. - Check
glxinfo -Band see if you’re onllvmpipe.
If you’re on fallback: stop. Fix that first. Everything else is lipstick on a framebuffer.
Second: isolate whether you’re CPU-bound, memory-bound, or IO-bound
vmstat 1while reproducing lag: look for highus/sy(CPU), highsi/so(swap), highwa(IO wait).iostat -xz 1if swap or IO wait appears.perf statas a quick signal for cache misses and poor IPC under load.
Decision: If you’re swapping or IO-bound, fix memory/disk first. If CPU is pegged, reduce software rendering load or enable acceleration.
If cache misses spike, reduce pixel touching and copying.
Third: reduce the pixel budget ruthlessly
- Drop resolution (
xrandr), then test again. - Reduce color depth if possible (legacy stacks still allow it; modern ones less so).
- Disable compositing and “smooth effects.”
Decision: If performance improves immediately, your bottleneck is bandwidth/copying, not “application logic.”
Fourth: validate redraw patterns
- Use
x11perffor a quick sanity check of 2D operations. - Watch resource growth with
xrestopto catch leaks or excessive buffering.
Decision: If the app is generating redraw storms, fix app invalidation and clipping rather than chasing driver flags.
Common mistakes (symptom → root cause → fix)
1) Mouse moves but windows “stick” during drag
Symptom: Cursor remains responsive; window drag leaves trails; redraw happens in chunks.
Root cause: Hardware cursor overlay works, but 2D acceleration is disabled; updates are software copies via shadow framebuffer.
Fix: Confirm driver binding; disable compositor; reduce color depth/resolution; ensure correct Xorg driver (modesetting vs vendor).
2) Smooth in the lab, slow in the field
Symptom: Same software; different sites; only some machines crawl.
Root cause: Different device revisions or firmware; driver falls back on specific PCI IDs; or memory is smaller causing swap.
Fix: Inventory hardware IDs; compare Xorg logs; pin known-good driver versions per model; enforce a minimum RAM spec.
3) “Upgraded visuals” causes stutter without high CPU
Symptom: UI stutters; CPU not pegged; user reports “sticky” feel.
Root cause: Bus/VRAM bandwidth saturation; higher bpp and alpha blending increased memory traffic; thread blocks in driver calls.
Fix: Reduce bpp/effects; clip redraw regions; replace per-pixel blends with precomposited assets; adopt dirty rectangles.
4) Random black frames or flicker after enabling double buffering
Symptom: Periodic blanking, especially under load; sometimes only on certain monitors.
Root cause: Full-frame blits miss refresh timing; page flipping not supported reliably; VRAM pressure triggers allocation failures.
Fix: Hybrid buffering: buffer only animated regions; use vsync/page-flip only when supported; reduce resolution; cap frame rate.
5) Performance degrades over hours
Symptom: Starts OK; gradually lags; reboot “fixes” it.
Root cause: Resource leak (pixmaps, offscreen surfaces), swap creep, log IO growth, or compositor cache ballooning.
Fix: Track resources (xrestop), memory (vmstat), disk (iostat); implement restart policy; patch leaks.
6) Tearing visible on animations and scrolling
Symptom: Horizontal tear lines during motion.
Root cause: No vsync/page flipping; direct writes to front buffer; compositor disabled or misconfigured.
Fix: Enable vsync where supported; use compositing on capable hardware; or reduce motion/refresh cost if hardware can’t keep up.
7) “We enabled acceleration” but everything got worse
Symptom: Higher CPU use, lower responsiveness after switching drivers.
Root cause: Acceleration path triggers expensive fallbacks (e.g., unsupported operations force readbacks); mismatched Mesa/driver.
Fix: Validate actual renderer (glxinfo -B); try alternate acceleration method (UXA/SNA/glamor depending on stack);
keep versions consistent; prefer fewer features over buggy features.
Checklists / step-by-step plan
Step-by-step: stabilize a legacy graphics environment
-
Inventory hardware. Capture
lspci -nnand store it with the machine profile.
Why: Different revisions behave differently; you can’t manage what you don’t name. -
Lock known-good drivers per hardware class.
Why: Graphics stacks regress. Treat them like kernels: staged rollout and pinning. -
Baseline performance. Record
x11perf -copywinwin500, resolution, compositor status.
Why: When someone says “it feels slower,” you can answer with numbers. -
Set a sane default mode. Pick a resolution and refresh that the hardware sustains.
Why: The cheapest performance win is fewer pixels. -
Decide on compositing explicitly. Either disable it or configure it; don’t let it “just happen.”
Why: Compositors add buffering and blending, which is exactly where old systems die. -
Budget memory. Ensure the system won’t swap under normal load.
Why: Swap turns small stutters into seconds of silence. -
Control redraw patterns in apps. Prefer dirty rectangles; avoid full-window repaints on timers.
Why: Writes to the framebuffer are your most expensive “API call.” -
Test with worst-case assets. Largest fonts, busiest screens, maximum data density.
Why: If it barely passes in ideal conditions, it will fail in production. -
Define degradation modes. Lower animations, reduce alpha, fall back to simpler rendering if performance drops.
Why: A stable “less pretty” UI beats a pretty UI that locks up. -
Maintain rollback capability. Keep last-known-good packages and configs.
Why: When graphics breaks, you often need to recover without a working local UI.
Checklist: pre-change validation (driver update, OS refresh, new UI)
- Confirm kernel driver binding (
lspci -k). - Check renderer path (
glxinfo -B). - Record current mode (
xrandr --current). - Run a quick 2D benchmark (
x11perftest subset). - Confirm compositor choice and configuration (
ps -ef). - Verify no GPU hangs in logs after stress (
dmesg). - Check memory headroom under load (
vmstat). - Verify disk is not saturated (
iostat), especially on thin clients with cheap flash.
Checklist: emergency response when the UI is unusable
- Switch to a lower resolution via remote shell where possible.
- Disable compositing and restart the session.
- Roll back the graphics stack packages to baseline.
- If stuck on fallback fbdev, boot with known-good kernel parameters and confirm module load.
- Reduce app redraw load: disable animations, lower update frequency, simplify visuals.
FAQ
1) What did “graphics card” actually do before GPUs?
Often: scan out a frame buffer to the monitor, manage a palette, and maybe accelerate 2D operations like rectangle copies and fills.
The CPU still drew most things, especially anything that wasn’t a simple block transfer.
2) Why was 320×200 so common in old PC games?
Because it was fast and simple. It fit well in memory, mapped linearly in the famous 256-color mode, and didn’t demand much bandwidth.
Games could update a decent portion of the screen without falling off a performance cliff.
3) What’s a “blitter,” and why should I care?
A blitter is hardware dedicated to moving blocks of pixels (BitBLT) and sometimes doing simple raster operations.
It mattered because UI and 2D games are dominated by “copy this rectangle,” not “compute this triangle.”
In operations terms, it’s a DMA engine for pixels.
4) Was software rendering always slow?
Not always. It was often surprisingly competitive because the CPU could be decent at math, and clever programmers minimized memory writes.
But it was fragile: one extra pass over the frame buffer, one more blend operation, or a higher color depth could collapse performance.
5) Why did color depth changes cause disasters?
Because bandwidth and storage scale with bytes per pixel. Jumping from 16bpp to 32bpp doubles frame buffer traffic.
On older buses and integrated graphics, that’s not a “small” change; it can be the whole budget.
6) How did people avoid flicker without modern compositors?
Dirty rectangles, careful ordering of draws, timing updates to vertical blank when possible, and sometimes page flipping
if the hardware supported multiple buffers. They also designed UIs that didn’t redraw constantly—because they couldn’t.
7) What’s the modern equivalent of a “framebuffer bottleneck”?
Any time you’re dominated by memory copies and bandwidth: software compositing, remote desktops pushing full-screen updates,
or applications that force GPU readbacks. The labels changed; the physics didn’t.
8) If my system shows llvmpipe, is that always bad?
It depends on workload. For static dashboards or light 2D, it might be acceptable. For heavy compositing, video, or 3D, it’s usually a problem.
The practical move is to treat llvmpipe as a capacity signal: you’re spending CPU to do graphics.
9) What’s the single highest-leverage tuning knob on old graphics systems?
Reduce pixels touched per second. That usually means lower resolution, fewer full-window repaints, fewer blends, and less compositing.
If you must optimize, optimize redraw strategy before micro-optimizing math.
10) Why do legacy graphics issues feel “random”?
Because they often sit at the intersection of hardware revisions, firmware quirks, and driver fallbacks.
Two machines that look identical to procurement can behave differently to a driver.
This is why baselines and hardware IDs matter.
Conclusion: practical next steps
Before NVIDIA and the GPU era, “graphics” was a budget you could count: bytes per pixel, pixels per frame, frames per second,
and the bus that had to carry it all. The engineering culture that came out of that period—dirty rectangles, palette tricks,
careful buffering, and suspicion of “free” visual upgrades—wasn’t nostalgia. It was what worked under hard constraints.
If you’re operating or modernizing anything that still smells like that era (embedded kiosks, thin clients, industrial HMIs,
remote desktops, legacy X11 apps), do three things this week:
- Prove the rendering path. Confirm the real driver and renderer in use; banish silent fallbacks.
- Baseline and pin. Capture logs and simple benchmarks, and pin a known-good graphics stack per hardware class.
- Cut redraw. Reduce pixel touching: resolution, color depth, compositing, and repaint strategy.
The pre-GPU world wasn’t kinder. It was just more honest: you could see the bottleneck with your eyes.
Treat that honesty as a diagnostic advantage, and you’ll keep even stubborn legacy graphics stable enough for business to happen.