A lengthy period ago, whenever I was a university undergrad, I spent several period operating on device video games. This was in the 8-bit PC era, whenever the gaming hardware was nearly impossibly dilatory by today’s standards.
It power not astonishment you, then, to study that equivalent programmers of yore did all sorts of insane things to create their games run at playable speeds. Crazy, insane things.
This is a narrative concerning one of those things.
While I’ve done my finest to recall the crucial details, I may have gotten several things wrong. If I have, delight forgive me. It was a lengthy period ago.
(Note: This article contains several small images as inline-data URLs that several nourish readers apparently don’t grip well. If you can’t see the images in your feed, click through to the first part to see them.)
Update 2013-12-16: There have been many of awesome comments concerning this narrative on Hacker News and Proggit, as fine as here on the blog. Thanks for sharing, everyone!
The set-up
A companion of mine, a gifted programmer, was nearly completed alongside a new game. Somehow, he had squeezed into a 1980’s-era PC, beautiful faithfully, what was at the period a graphically notable coin-op equivalent famous at the arcades.
The lone issue was that his type of the equivalent was unplayable. It was too slow, and the choppy motion bankrupt the player’s immersion. It was following all a side-scroller.
My friend, who had been operating on the equivalent during additionally taking a complete burden of university courses, was starting to appear a small frazzled. Fearing that he had missed several uncomplicated optimization, he asked me to obtain a appearance at the code.
I did. But there was no uncomplicated optimization to be had.
The code was tight. Everywhere I looked, he had already done all the things I could think of. Loops had been unrolled. Unneeded draws had been eliminated. All evidence of discarded had vanished.
And, alongside them, our hopes of an uncomplicated fix.
But what concerning a difficult fix? A insane fix?
Well, that was continually a possibility.
Before I get to the insane stuff, however, I should rear up and explain how graphics hardware worked rear then.
How graphics hardware worked rear then
Here’s the quick version: The graphics hardware didn’t do jack squat.
On the PC in question, if you wanted to diagram item onto the screen, you had to do that yourself, byte by byte. No texture mappers, no blitters. Just bytes. Bytes that you had to move yourself.
Fun.
My friend’s equivalent spent most of its period redrawing the background. (Again: side-scroller.) For all frame, it had to diagram nearly a screen’s value of backdrop tiles, shifted for the player’s position.
If my recollection serves me, all tile was 28 by 28 pixels. Each pixel was one of 16 colors, taking fractional a byte to represent. Therefore, tiles were represented in recollection as 28 adjacent rows of 14 bytes each. The archetypal 14 bytes represented the archetypal row, the second 14 bytes the second row, and so on.
The screen, however, was 320 pixels broad by 240 pixels high. In memory, then, a display buffer was laid out as 240 adjacent rows of 160 bytes each.
So whenever copying a tile at location X onto a display buffer location starting at location Y, you had to copy 28 rows. To copy all row, you had to copy 14 bytes. Fortunately, the processor for this equivalent was a 6809, which had a few 16-bit indicator registers and delightful “auto-increment” addressing modes (sort of akin applying the postfix ++ controller to pointers in C). That meant you could copy 4 pixels at a period during advancing the two the X and Y registers in passing:
LDU ,X++ ; peruse a 2-byte term (= 4 pixels) from origin tile STU ,Y++ ; compose it to display buffer
To copy a complete row, you had to do that seven times, so you power sandwich those lines inside a 7-count loop:
LDB #7 ; remaining words <- tile width in words @LOOP LDU ,X++ ; peruse a 2-byte term (= 4 pixels) from origin tile STU ,Y++ ; compose it to display buffer DECB ; decrease remaining-word count BNE @LOOP ; iteration during words remain
When you were done copying the row, you needed to advancement the destination pointer Y so that it pointed to the starting location for the next row you would diagram into the display buffer. Since the display buffer was 160 bytes broad and a tile was lone 14 bytes wide, that meant you had to add their difference to Y:
LEAY 160-14,Y
And, fair akin that, you have copied a row onto the screen.
That’s one. To copy a complete tile, you need to do the identical item 28 times. So, in turn, you sandwich that code inside a 28-count loop.
Putting it all together, and giving names to the crucial numbers, you power end up alongside a subroutine akin this:
;;; crucial constants SCRW = 160 ; screen-buffer width in bytes (= 320 4-bit pixels) TILW = 14 ; background-tile width in bytes (= 28 4-bit pixels) TILH = 28 ; background-tile height in rows WOFF = SCRW - TILW ; s-b offset from end of one tile row to commencement of next COPYTILE ;;; ;;; Copy a 28x28 backdrop tile into a display buffer. ;;; Arguments: ;;; X = starting location of backdrop tile ;;; Y = starting location of destination in display buffer ;;; LDA #TILH ; remaining rows <- tile height @COPYROW LDB #TILW/2 ; remaining words <- tile width in words @LOOP LDU ,X++ ; peruse a term (= 4 pixels) from origin tile STU ,Y++ ; compose it to display buffer DECB ; decrease remaining-word count BNE @LOOP ; iteration during words remain ;; LEAY WOFF,Y ; advancement dst ptr to commencement of next dst row DECA ; decrease remaining-row count BNE @COPYROW ; iteration during rows remain ;; RTS ; done! come back to caller
And that code would be fine.
If you didn’t attention concerning speed.
Caring concerning speed
Knowing that the equivalent is likely going to expend most of its period operating that code, you do what any fine programmer would: commencement counting cycles. Here’s the inner iteration again, alongside setup and finishing, annotated alongside sequence counts:
LDB #TILW/2 ; 2 cycles (set-up) @LOOP LDU ,X++ ; 8 STU ,Y++ ; 8 DECB ; 2 BNE @LOOP ; 3 ;; LEAY WOFF,Y ; 8 (finishing)
Looking at those counts inside the loop, you’re not apt to young female that you’re burning 21 cycles to copy fair 4 pixels. To copy a complete row, then, plant out to 2 cycles + (7 iterations) * (21 cycles/iteration) + 8 cycles = 157 cycles. Ouch.
But this isn’t your archetypal period at the keyboard. You cognize what to do. Unroll that loop!
LDU ,X++ ; 8 cycles STU ,Y++ ; 8 LDU ,X++ ; 8 STU ,Y++ ; 8 LDU ,X++ ; 8 STU ,Y++ ; 8 LDU ,X++ ; 8 STU ,Y++ ; 8 LDU ,X++ ; 8 STU ,Y++ ; 8 LDU ,X++ ; 8 STU ,Y++ ; 8 LDU ,X++ ; 8 STU ,Y++ ; 8 LEAY WOFF,Y ; 8 (finishing)
Now, alongside the iteration overhead reduced to zero – you’ve equal eliminated the set-up – all row takes lone 7 * (8 + 8) + 8 = 120 cycles. That’s a 30-percent speed-up. Pretty good.
And that’s anywhere most programmers likely would have remaining it.
But not my friend.
He knew that those ++ operations were costly, 3 cycles apiece. And, alongside the iteration unrolled, he additionally knew exactly anywhere all term to be peruse or written was located alongside regard to X or Y. So he cleverly replaced those 3-cycle post-increments alongside exact offsets. They disbursal lone 1 sequence apiece, and the 0 offset is really free:
LDU ,X ; 5 cycles STU ,Y ; 5 LDU 2,X ; 6 STU 2,Y ; 6 LDU 4,X ; 6 STU 4,Y ; 6 LDU 6,X ; 6 STU 6,Y ; 6 LDU 8,X ; 6 STU 8,Y ; 6 LDU 10,X ; 6 STU 10,Y ; 6 LDU 12,X ; 6 STU 12,Y ; 6 LEAX TILW,X ; 8 (finishing) LEAY SCRW,Y ; 8 (finishing)
With his optimizations, the sequence figure per row had been reduced to (5 + 5) + 6 * (6 + 6) + (8 + 8) = 98 cycles. Compared to the first code, that’s 60 percent faster:
original_speed = (1*row) / (157*cycle) optimized_speed = (1*row) / (98*cycle) speed_up = optimized_speed / original_speed - 1 = 157 / 98 - 1 = 0.60.
Putting it all together – and, again, I’m going by recollection so the code power have been slightly distinct – the copy-tile subroutine looked item akin this, and it copied a complete tile, all 28 rows, in a lean 2893 cycles:
COPYTILE2 ;;; ;;; Copy a 28x28 display tile into a display buffer. ;;; Arguments: ;;; X = starting location of backdrop tile ;;; Y = starting location of destination in display buffer ;;; Execution time: ;;; 4 + 28 * (82 + 8 + 8 + 2 + 3) + 5 = 2893 cycles ;;; LDA #TILH ; initialize row figure (4 cycles) ;; @COPY1 ;; unroll inner iteration (copies one row of 28 pixels in 82 cycles) LDU ,X ; (1) peruse 4 pixels (5 cycles) STU ,Y ; compose 4 pixels (5 cycles) LDU 2,X ; (2) (6 cycles) STU 2,Y ; (6 cycles) LDU 4,X ; (3) ... STU 4,Y ; ... LDU 6,X ; (4) STU 6,Y ; LDU 8,X ; (5) STU 8,Y ; LDU 10,X ; (6) STU 10,Y ; LDU 12,X ; (7) STU 12,Y ; ;; LEAX TILW,X ; advancement src to commencement of next row (8 cycles) LEAY SCRW,Y ; advancement dst to commencement of next row (8 cycles) DECA ; decrease remaining figure by one (2 cycles) BNE @COPY1 ; iteration during rows remain (3 cycles) ;; RTS ; done! come back to visitant (5 cycles)
This code, all in all, was 60% faster than the naive COPYTILE code we started out with.
But that wasn’t accelerated enough. Not nearly.
So whenever my companion showed me his code and asked if I could create it faster, I really wanted to assistance him. I really wanted to be capable to answer yes.
But I had to answer no. I hated giving that answer. But, studying the code, I couldn’t see any way to speed it up.
And yet, the hook was set.
Later, I couldn’t get the issue out of my head. Maybe I had missed something. I had grown up on Apple II computers and their 6502 processors. My friend’s code, however, was for the 6809. Maybe it afforded optimizations that I didn’t cognize about.
With renewing optimism, I dialed into the university’s archive to admission the cardstock catalog. (These were the days before the World Wide Web.) Through a VT220 terminal emulator, I searched for books concerning the 6809.
There was one. A 6809 microprocessor manual. It was in the engineering library. Luckily, I was an engineering pupil and had sign-out privileges.
Just akin that, I was off to the library.
A insane idea
When I got to the engineering library, I established the publish correct anywhere it was expected to be, looking a small attack up:
The MC6809-MC6809E Microprocessor Programming Manual.
Not bothering to discover a chair, I flipped through the pages standing, looking for several oddball, 6809-specific education that power throw a lot of bytes and throw them fast. Page following page, though, turned up nothing.
Then I came upon PSHS. “Push registers on the hardware stack.”
On my cherished 6502, if you wanted to preserve registers on the stack, you had to do it one enroll at a period and, equal then, by transferring them through the accumulator. It was dilatory and expensive. So I had learned to evade the stack whenever speed mattered.
But on the 6809 you could preserve all of the registers – or any subset of them – alongside a sole instruction. And, amazingly, it disbursal a uncomplicated 5 cycles, affirmative 1 sequence additional per byte pushed.
Since the processor had three 16-bit general-purpose registers – D, X, and Y – I could burden them up and afterward use a sole PSHS education to compose 6 bytes in fair 11 cycles. The corresponding drag instruction, PULS, had the identical low cost.
Further, the 6809 had two stack registers, S and U. I could use one as a origin pointer and the another as a destination pointer. In theory, alongside a sole PULS/PSHU pair, I could copy 6 bytes in 22 cycles.
That’s crazy, insane fast.
Excitement mounting, I headed for the forefront array and reached for my pupil ID. I was going to sign out that fantastic small publish for additional study.
A insane plan
Walking rear to the dorms, I cast my plan.
I would preserve the S and U registers somewhere, and afterward item S at the backdrop tile and U at the display buffer. Then I would drag from S and shove to U, copying 6 bytes at a time, using D, X, and Y as go-betweens. To copy the 14 bytes that create up a row would necessitate three specified iterations, which unrolled would be concerning 60 cycles.
When I reached my room, I established a part of document and sketched it out:
PULS D,X,Y ; archetypal 6 bytes 11 cycles PSHU D,X,Y ; 11 PULS D,X,Y ; second 6 bytes 11 PSHU D,X,Y ; 11 PULS D ; final 2 bytes 7 PSHU D ; 7 LEAU -WOFF,U ; advancement dst ptr 8
Just 66 cycles, including the after-row adaptation to U that prepares it for the next row. (Note that the adaptation is now negative.) For comparison, the naive row-copy iteration that we looked at before took 157 cycles to do the identical thing. And my friend’s optimized code took 98. Already, this insane idea was looking akin a big win.
Still, that final PULS/PSHU pair! What a clunker. It was needed to grip the final two bytes per row, since the rows were 28 pixels = 14 bytes wide, and 6 doesn’t distinct 14 evenly.
That darn remainder of 2 bytes!
If lone the equivalent had used 24-by-24 tiles instead… But it hadn’t, so I pored through the manual, looking for a way to lesser the disbursal of that inevitable final pair.
And then, by surprise, I struck gold! It was another 6809 oddity, the DP register.
On the 6502 and most 8-bit processors of the era, the lowest 256 bytes of recollection was called the zero page. The zero leaf was particular since its recollection locations had single-byte addresses and could be accessed alongside shorter and normally faster instructions.
The 6809’s designers took this idea one stage further. They let you use the DP enroll to designate any leaf as the zero page, which they called the “direct page.”
But none of my byte-slinging instructions needed to use the straightforward page. That meant I could use the DP enroll as an additional one-byte go-between. Now I could copy 7 bytes per pull-push pair!
And 14 is evenly divisible by 7.
With this change, I could copy a entire 28-pixel row and advancement to the next in fair 5 instructions:
PULS D,X,Y,DP ; archetypal 7 bytes 12 cycles PSHU D,X,Y,DP ; 12 PULS D,X,Y,DP ; second 7 bytes 12 PSHU D,X,Y,DP ; 12 LEAU -WOFF,U ; advancement dst ptr 8
56 cycles! Fifty. Six. Cycles.
That bit of code made me awareness fair awesome. I had managed to harness all sole accessible enroll on the device for byte-slinging! D, X, Y, U, S, and equal the oddball DP – they were all completely engaged.
I was emotion awesome concerning this solution.
Except for fair one thing…
I’ll have a small additional crazy, please
If you’re acquainted alongside stacks, you may have noticed a subtle flaw in my brilliant plan:
Pushes and pulls activity in contrary directions.
See, I lied concerning that small obstacle of code I fair showed you. It didn’t actually copy one row from a backdrop tile onto the screen. What it really did was interrupt the row into two 7-byte blocks – I’ll call them “septets” – and afterward diagram them onto the display swapped.
Recall that tile rows were laid out sensibly in recollection as 14 adjacent bytes, akin this:
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 0 1 2 3 4 5 6 7 8 9 A B C D | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+
So whenever you pulled-and-pushed a row onto the display in 7-byte septets, it would get horizontally divided akin this:
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+ | 7 8 9 A B C D | 0 1 2 3 4 5 6 | +---+---+---+---+---+---+---+---+---+---+---+---+---+---+
The archetypal septet was now second, and the second now first. Note, however, that the bytes inside all septet were unchanged; they retained their first ordering.
Further, if you ran the row-copy code multiple times, the vertical stack of rows you copied would end up being upside down. That’s since the code uses pushes to compose the rows onto the screen. Pushes move the stack pointer toward lesser addresses, and lesser addresses equivalent to display lines higher up on the display.
To visualize the havoc that this row-reversal and septet-swapping would wreak, let’s say that you had the following 28-by-28 tile, representing a key:
If you had used 28 applications of my row-copy code to diagram it onto the screen, you would have ended up alongside this not-a-key:
So . . . that was a problem.
But this problem, too, had an elegant solution!
To review:
- the rows were upside down
- the septets inside all row were swapped
- but the bytes inside all septet were unchanged
The breakthrough was to see “upside down” and “swapped” as two instances of the identical thing. Reversal.
Reversal has this helpful property: it’s an involution. Reverse item twice, and you get rear the original. With this in mind, I justified that if the tiles were pre-processed to reverse their rows and afterward the septets inside the rows, the tiles would end up looking fine whenever they hit the screen. The mangling that occurred during copying and pre-processing would, in effect, undo all other.
Working it out on paper, I additionally discovered that the pre-processing stage could disregard the rows and fair treat the tiles as one long, linear sequence of septets.1 To see why this is so, let’s regard a small tile of 2 rows of 2 septets each:
+---+---+ | a b | +---+---+ | c d | +---+---+
In memory, it would be laid out in row-major order, that is, as 4 sequential septets:
+---+---+---+---+ | a b | c d | +---+---+---+---+
So, reversing the rows and afterward swapping the septets inside all row is the identical as fair reversing the septet command in memory:
+---+---+ +---+---+ +---+---+ | a b | | c d | | d c | +---+---+ =======> +---+---+ =======> +---+---+ | c d | reverse | a b | toggle | b a | +---+---+ rows +---+---+ septets +---+---+ +---+---+---+---+ +---+---+---+---+ +---+---+---+---+ | a b | c d | | c d | a b | | d c | b a | +---+---+---+---+ +---+---+---+---+ +---+---+---+---+
So the resolution to the mangling issue turned out to be a simple, one-time pre-processing step: Just interrupt all tile into septets and reverse their order.
Knowing that the mangling issue could be solved, I was emotion fine concerning my general idea. I couldn’t see any another problems, so I sketched out a new copy-tile subroutine to current to my friend. Since the row-copy logic was now fair 5 instructions, I used it 4 times, unrolling the sole remaining iteration a bit. Now, alternatively of copying 28 sole rows, I copied 7 quad-rows.
The code looked item akin this:
COPYTILE3 ;;; ;;; Copy a 28x28 display tile into a display buffer. ;;; Arguments: ;;; X = starting location of *septet-reversed* backdrop tile ;;; Y = starting location of destination in display buffer ;;; Execution time: ;;; 34 + 7 * (224 + 7 + 3) + 7 + 10 = 1689 cycles ;;; ;; setup: 34 cycles PSHS U,DP ; preserve U and DP (8 cycles) STS >SSAVE ; preserve S (7 cycles) ;; LDA #TILH/4 ; first quad-row figure (2 cycles) STA >ROWCT ; (5 cycles) ;; LEAS ,X ; initialize src ptr (4 cycles) LEAU (TILH-1)*SCRW+TILW,Y ; initialize dst ptr (8 cycles) ;; @COPY1 ;; copy four rows of 28 pixels in 4 * (48 + 8) = 224 cycles PULS X,Y,D,DP PSHU X,Y,D,DP PULS X,Y,D,DP PSHU X,Y,D,DP LEAU -WOFF,U PULS X,Y,D,DP PSHU X,Y,D,DP PULS X,Y,D,DP PSHU X,Y,D,DP LEAU -WOFF,U PULS X,Y,D,DP PSHU X,Y,D,DP PULS X,Y,D,DP PSHU X,Y,D,DP LEAU -WOFF,U PULS X,Y,D,DP PSHU X,Y,D,DP PULS X,Y,D,DP PSHU X,Y,D,DP LEAU -WOFF,U ;; DEC >ROWCT ; decrease remaining quad-row figure by one (7 cycles) BNE @COPY1 ; iteration during quad-rows remain (3 cycles) ;; LDS >SSAVE ; reconstruct S (7 cycles) PULS U,DP,PC ; reconstruct regs and come back to visitant (10 cycles) SSAVE ZMD 1 ; stash for preservation S during drawing ROWCT ZMB 1 ; var: remaining rows to copy
Adding up the cycles, the total was fair 1689. I had shaved nearly 1200 cycles off of my friend’s code. That’s a 70 percent speed-up!
Beaming, I went to display my friend.
The acidic test
When I caught up alongside my companion and stated I’d figured out how to create the copy-tile regular 70% faster, his visage lit up. When I explained concerning the entire septets-and-mangling thing, though, his visage unlit, doubts rising. But whenever I showed him the code, he got it. It clicked.
“Let’s try it out!” he said.
In concerning a fractional hour’s work, he had the code unified into the game. After a rebuild and reboot, the equivalent was loading.
The heading display came up.
He was into the game.
And . . .
Holy crap!
The equivalent seemed amazingly fast. In truth, it was likely lone a third to a fractional faster, but that was enough. Some crucial perceptual threshold had been crossed. The game-play was now smooth, natural. You could awareness the difference.
We fair sat there, playing the equivalent and grinning. It was a fine day.
But it would not last.
In for a penny . . .
A few days following the speed issue was rearward us – or so we idea – the game’s finishing touches were applied. One of those finishing touches was sampled audio effects. This required feeding byte-sized morsels to the audio-output DAC a few thousand times all second. To agenda the feedings, my companion had turned on a hardware-clocked interrupt.
Worked akin a charm. The appears echoed great, and everything was perfect.
Except for one thing.
After playing into the equivalent one night, we noticed that several of the tiles were getting corrupted. The additional we played, the additional misconduct we saw.
Oh, no.
Something extremely bad was happening.
And afterward it hit us.
What happens if an interrupt goes off during the copy-tile routine?
The processor, to prepared for calling the interrupt handler, would shove its current province onto the scheme stack. But, during the copy-tile routine, there was no scheme stack. The system-stack enroll S had been commandeered. And anywhere was it pointing? Right into the recollection buffer that held the citation tiles!
Oops.
We slumped. After all that effort, to think that our all-important speed-up was causing recollection corruption…
Needing a break, we walked to an all-night diner near evidence to eat and think. Over pancakes and bacon, scrapple and grilled stickies, we talked through the problem.
There were lone two stack registers, and without commandeering the two of them, the copy-tile regular wouldn’t be nearly as fast. There was no way, then, to come back the S enroll to the scheme without losing our hard-won speed. But there was additionally no way that we could get dependable audio without using interrupts.
One way or another, then, interrupts were going to be triggered. And, whenever they were, if copy-tile was running, any S was pointing to was going to get hammered.
“How can we forestall the corruption?” I asked.
We sat there, ignoring our food, the inquiry hanging in the air.
Suddenly, my companion slapped the table. He had it.
“Don’t forestall it!” he said.
“What?” I asked.
“Don’t forestall the corruption,” he explained. “Let it happen. Just not to the citation tiles.”
It was simple, really. He continued:
“Just toggle S and U in the copy-tile routine. U volition item at the citation tiles, and S at the display buffer. If an interrupt goes off, the misconduct volition happen anywhere S is pointing – on the screen. The misconduct volition final lone until we redraw the next frame.”
“That’s brilliant!” I said.
Eager to try his solution, we quickly completed our meal.
. . . In for a pound
Walking rear to the dorms, though, it bothered us that players power see display glitches, equal if for fair a frame. We the two felt that there had to be several way to create the hack perfect, if lone we could discover it.
Later that night, we established it.
Again, it was uncomplicated formerly we saw it. All we had to do was alter the tile ordering. Instead of placing the tiles onto the display from top to bottom, remaining to right, we would go correct to left, base to top. In another words, from elevated addresses to low addresses.
That way, whenever an interrupt went off and corrupted the recollection locations immediately before the tile we were currently drawing, the misconduct would be fixed whenever the extremely next tile was drawn. And since tile-drawing took location in a buffer that wasn’t displayed until it was completely drawn (and the vertical refresh had occurred), nobody would always see the corruption.2
It was the ideal hack!
To the old days!
And that’s the way things were. The difficulty wasn’t overwhelming complexity, as it is today. The difficulty was cramming your ideas into machines so slow, so constricted that most ideas didn’t fit.
You had to bang your ideas around, twist them, rotate them, searching for something, anything that would assistance you compression them into the machine. Sometimes you established it, and you got one stage nearer to realizing your ideas. Sometimes you didn’t.
But the hunt was continually instructive.
In this case, the hunt yielded multiple small victories that, together, solved the problem. But whenever you regard all the things we had to do to create that darn equivalent accelerated enough, it does appear crazy.
We started alongside a tile-copying subroutine whose center was a hand-tuned, unrolled iteration of device instructions. Then, to buy a 70-percent speed-up:
- We replaced this subroutine alongside a extremely particular manifestation of insanity that commandeered the two stack pointers and used pulls and pushes, and all sole accessible register, to diagram tiles upside downward and horizontally broken.
- Then we pre-processed the tiles so that example them would really fix them.
- But afterward – dammit! – interrupts that occurred during example could corrupt the citation tiles.
- So, to defend the citation tiles, we corrupted the display buffer instead.
- But that misconduct would be visible.
- So we changed the tile-placement command to repair – on the fly – any misconduct that power have occurred, before it could always be displayed.
- And it all worked!
We did that. For 70 percent.
And it was so value it.
Tell ’em if you got ’em
Anyway, I wanted to portion that narrative alongside you for two reasons.
The archetypal is that it’s a fun story. It’s one my earliest memories of struggling alongside a messy computing issue and, through dogged persistence, zeroing in on a resolution that was the two productive and elegant. The outcome was powerfully satisfying.
The struggle, I learned, is value it.
The second logic is to advance you to inform your stories. I cognize that, “back in the day,” most video games likely gave increase to many stories akin this one. I’d affection to comprehend them. But too many of them are lost, having faded out of recollection before anyone idea to maintain them.
If you have a story, delight don’t wait. Tell it. Every day you wait, it gets harder.
Tell it.