ZX Online – Modern ZX Spectrum Games https://zxonline.net/ ZX Spectrum games are living here Sat, 31 Jan 2026 23:41:06 +0000 en-GB hourly 1 https://wordpress.org/?v=6.9.4 https://zxonline.net/wp-content/uploads/2018/07/cropped-zxonline-icon-256-32x32.png ZX Online – Modern ZX Spectrum Games https://zxonline.net/ 32 32 Rabbits, Gold, and Old-School Hardcore: The Tale of Rabbits is Here https://zxonline.net/rabbits-gold-and-old-school-hardcore-the-tale-of-rabbits-is-here/ https://zxonline.net/rabbits-gold-and-old-school-hardcore-the-tale-of-rabbits-is-here/#respond Sat, 31 Jan 2026 23:36:50 +0000 https://zxonline.net/?p=155685 The post Rabbits, Gold, and Old-School Hardcore: The Tale of Rabbits is Here appeared first on ZX Online - Modern ZX Spectrum Games.

]]>
A year ago, the ZX Evolution team dropped Lone Lunner. I remember being genuinely stoked that someone finally built a proper, good-looking Lode Runner for the Spectrum – instead of those sad, monochrome homebrew projects we got used to in the 90s.

Now, out of nowhere – a sequel. Meet The Tale of Rabbits.

The core loop is classic: run around, grab gold (well, carrots now!), dig holes, and run for your life. But the devs didn’t just swap out the sprites. This is a full-blown sequel running on that same clever engine that handles two seemingly different platforms at once: the western ZX Spectrum Next and the eastern TS Conf.

Honestly, that tech aspect is what gets me. Getting these two pieces of hardware to play nice together is no small feat. The fact that these guys keep polishing their cross-platform engine deserves serious respect. It’s not just “another game” – it’s proof that these scenes can actually be bridged.

What’s inside:

  • Visuals: Still rocking that pleasant style that feels like a mix of NES and Sega. Bright, juicy, readable.

  • Gameplay: Pure hardcore. If you played the first one, you know the enemies aren’t just following a script – they’re actively trying to end you. The AI here is sharp again. This isn’t a casual stroll; it’s a reflex puzzle. The first level actually drove me up the wall – it took me 2.5 hours to beat! Thank god they included level codes so you don’t have to restart from scratch every time.

  • Setting: As the name suggests, it’s about rabbits now. It looks cute, but trust me, your hero is going to die just as often.

I genuinely don’t know where the devs find the patience to code for retro platforms in 2026, but I’m glad they do. If you have a Next or a TS-Conf, you can’t skip this.

Where to grab it & how to support:

The game is already up on ZX Online. I highly recommend buying it there. First, it’s cheaper. Second, payments are painless – Robokassa handles both Russian and worldwide cards without issues, and you can even pay via Volet or crypto. There’s a version on itch.io, but the price tag is higher (fees and transfers eat up a chunk of the money before it reaches the creators).

So download it, play it, and don’t say I didn’t warn you about the difficulty. And seriously, drop the authors a kind word – enthusiasm costs a lot these days.

One more thing: The ZX Evolution crew launched their own Telegram channel – worth a look. They’re currently wrapping up their next big project: an RTS for the ZX Spectrum Next and TS Conf. No idea how it plays yet, but it looks killer. You can also support them on Boosty just like before.

Stay tuned!

The post Rabbits, Gold, and Old-School Hardcore: The Tale of Rabbits is Here appeared first on ZX Online - Modern ZX Spectrum Games.

]]>
https://zxonline.net/rabbits-gold-and-old-school-hardcore-the-tale-of-rabbits-is-here/feed/ 0
Turtles Fighters is now out! https://zxonline.net/turtles-fighters-is-now-out/ https://zxonline.net/turtles-fighters-is-now-out/#respond Wed, 10 Sep 2025 18:43:08 +0000 https://zxonline.net/?p=155488 The post Turtles Fighters is now out! appeared first on ZX Online - Modern ZX Spectrum Games.

]]>

Greetings to all lovers of retro and classics!

I am happy to announce that just a couple of days ago, a brand new game, Teenage Mutant Ninja Turtles Fighters, saw the light of day. This fan-made fighting game about the legendary Ninja Turtles is now available for download! An unexpected but very pleasant gift for all retro gaming fans, especially for those who own TS Conf and Sprinter spectrum clones. And for those who don’t have retro hardware, there is also a Windows version with a built-in emulator.

About the Game

Teenage Mutant Ninja Turtles Fighters is a 1-vs-1 fighting game created from scratch by the talented developer Alexander Tmk deMarche and the ZX-FANS-TEAM. The game engine is completely original and has its own features. The character graphics are inspired by the classic SNES version of the game, while the other elements are drawn from scratch or redrawn based on images found online. Special attention should be paid to the atmosphere and detailed drawing of the characters and locations, which are incredibly dynamic and create the effect of a complete three-dimensional immersion. And each character has 120 different poses, isn’t that cool?

The game was created so that you can spend time with friends, arranging a real tournament, and playing for the winner’s golden cup! There are four playable characters to choose from: Leo, Don, Mike, and Raph. Each of them has its own unique features and super moves. You have to choose which of the heroes will become your favorite to defeat all rivals.
Well, if you’re spending the evening alone, no problem, the game has a built-in bot that will easily keep you company, and it won’t be that easy to beat it!

Controls and Super Moves

The game controls are designed for a 6-button joystick, supporting Sega and 8bit-do standards. Each hero has close and long-range attacks, and when you are close, the set of attacks changes slightly. Throws are performed by pressing back + X or Y when you are close to the opponent. And to dodge an attack, you can do a back somersault by pressing back, back. In this mode, you are invincible.

Here is a list of super moves for each character, which can also be viewed in pause mode:

LEO:

  • down, back, hand — energy boomerang
  • back, down, forward, hand — sword dance
  • back, forward, hand — somersault with swords

DON:

  • down, back, hand — earth spike
  • back, down, forward, hand — Bo staff
  • down, forward, foot — head spin

MIKE:

  • down, back, foot — dragon’s breath
  • back, forward, forward, foot — shell flight
  • down, down, hand — nunchaku power

RAPH:

  • down, back, foot — fireball
  • back, down, forward, hand — electric drill
  • back, forward, foot — somersault punch

I’m sure the game will find many fans among Spectrum users, and even though there is no version for ZX Spectrum Next yet, you can write if you would be interested in having such a version.
Download, share your impressions, and support the developers with a kind word and a ringing coin. The price starts from 0 EUR, so this is a great reason to spend an evening with friends playing your favorite game!

The post Turtles Fighters is now out! appeared first on ZX Online - Modern ZX Spectrum Games.

]]>
https://zxonline.net/turtles-fighters-is-now-out/feed/ 0
From Bytes to Billions: How the ZX Spectrum Laid the Foundations of Modern Algorithms https://zxonline.net/from-bytes-to-billions-how-the-zx-spectrum-laid-the-foundations-of-modern-algorithms/ https://zxonline.net/from-bytes-to-billions-how-the-zx-spectrum-laid-the-foundations-of-modern-algorithms/#respond Thu, 17 Jul 2025 00:38:56 +0000 https://zxonline.net/?p=155353 What We Didn’t Think About – How the "Rubber" Computer Changed the World Hey, folks! Remember those days when every megabyte of memory seemed like a whole world, and every processor cycle was worth its weight in gold? Of course, you do! After all, many of us are from that very generation that grew up […]

The post From Bytes to Billions: How the ZX Spectrum Laid the Foundations of Modern Algorithms appeared first on ZX Online - Modern ZX Spectrum Games.

]]>
What We Didn’t Think About – How the "Rubber" Computer Changed the World

Hey, folks! Remember those days when every megabyte of memory seemed like a whole world, and every processor cycle was worth its weight in gold? Of course, you do! After all, many of us are from that very generation that grew up on the pixelated wonders and captivating melodies of our beloved "Speccy"! Among the games released long ago, you can find gems that evoke warm feelings even decades later. And, of course, for the most part, it doesn’t matter what platform these games were released on, what their graphics and sound design were like (or if they even existed at all!).

ZX Spectrum Nostalgie

Today, I want to talk about something that might seem non-obvious but is of colossal importance: how our humble ZX Spectrum, that "rubber-keyed" computer, became a true forge for algorithms that still form the basis of our modern digital world. You’d be surprised, but the very tricks that developers used to squeeze the maximum out of 48 KB of memory and a 3.5 MHz Z80 became the foundation for high-performance libraries and even for the devices that surround us every day, for example, in the world of IoT (Internet of Things)!

Initially, when people remember the ZX Spectrum, they often focus on its limitations: a small amount of memory, a modest processor, and peculiar graphics. However, it was precisely these limitations that, paradoxically, became a powerful stimulus for innovation. Developers didn’t just put up with them; they were forced to think outside the box, invent new approaches, and create incredibly efficient solutions. This need to squeeze the maximum out of every available resource led to the birth of a whole class of algorithms so optimized that their principles remain relevant even decades later. Thus, what seemed like a drawback actually turned out to be fertile ground for the development of fundamental computational methods.

ZX Spectrum: A Survival School for Algorithms

Hardware Features of the ZX Spectrum

The heart of the "Speccy" was the Z80 processor, running at a modest 3.5 MHz. But here’s the most interesting part: this processor couldn’t multiply numbers in hardware! No division or multiplication "out of the box"! All these operations had to be implemented in software, which was incredibly slow. Can you imagine the challenge for game developers, where every millisecond counts?

ZX Spectrum Character

And what about memory? A mere 48 KB of RAM! That’s millions of times less than modern computers. On top of that, 16 KB of it was allocated for the screen buffer, which further constrained the space for code and data. The 256×192 pixel graphics mode with a palette of 15 colors added its own complexities. But the "funniest" part was the attribute-based graphics: color was not set for each pixel, but for an 8×8 pixel block. This meant that within one such block, there could only be two colors – an INK color and a PAPER color, plus brightness and a flash attribute. No hardware sprites, no smooth scrolling! Everything had to be drawn and moved in software, pixel by pixel, byte by byte.

How "Flaws" Became a Catalyst for Engineering Ingenuity

These limitations were not a sentence, my friends, they were a challenge! They forced developers to think outside the box, to squeeze the maximum out of every bit and every clock cycle. It was a real school of optimization, where every byte and every instruction mattered.

For example, without hardware multiplication, ZX Spectrum developers had to create their own software implementations of this operation, often using a series of additions and bit shifts. This required a deep understanding of the Z80 architecture and the ability to write the most efficient machine code possible. Similarly, when working with graphics, where there was no hardware support for sprites or scrolling, every graphical element had to be drawn and moved manually, byte by byte. This led to the development of sophisticated algorithms for fast screen updates, minimizing artifacts, and making effective use of the limited palette. Such conditions forced developers not just to write code, but to create fundamental computational methods that are now built into hardware or high-level libraries.

Why Standard BASIC Couldn’t Keep Up, and Why Integer Computations Became a Necessity

ZX Spectrum BASIC, while an excellent starting point for many of us, had one serious problem: it used floating-point for all numerical operations. And this wasn’t just slow – it was imprecise! "Numbers are stored to an accuracy of about nine and a half digits," the manual tells us, and provides examples where 1e10+1-1e10 yields 0 instead of 1. Can you imagine the pain for a game developer, where the precision of coordinates or physics is crucial?

This fundamental limitation of BASIC meant that for serious applications, especially games that required high performance and accuracy (e.g., for collision detection, physics, or complex graphics), developers could not rely on the built-in numerical operations. They were forced to completely bypass BASIC and write their own subroutines in Z80 assembly. It was there that they manually implemented integer algorithms and fixed-point arithmetic to avoid the slow and inaccurate floating-point operations. This was not just an optimization choice, but a direct consequence of the platform’s hardware and software limitations, which led to a deep dive into low-level computing.

Integer Gems: Algorithms Born from Limitations

ZX Spectrum Magic Crystal

So, what are these "integer algorithms" and "fixed-point arithmetic"? Imagine you have the number 3.14. In normal life, this is a floating-point number. Now, imagine you multiply it by 100 to get 314. Now it’s an integer! Fixed-point arithmetic is a way of representing fractional numbers using integers, where it is understood that the decimal (or binary) point is in a fixed, predetermined position. All operations are performed as with integers, and the point is only "put back" for display. This avoids slow and resource-intensive floating-point operations while maintaining acceptable precision.

Examples from the World of the ZX Spectrum

On the ZX Spectrum, where every byte and every processor cycle was precious, developers had to create not just games, but entire computational infrastructures that are provided ready-made in modern game engines today. This required not only creativity but also a deep understanding of how data is represented in memory, how the processor works, and how every single operation could be optimized.

  • Graphical Wonders Without Hardware Assistance
    Remember how smoothly sprites moved in your favorite games, or how lines and circles were drawn? All of this was the result of incredible ingenuity! Since the "Speccy" had no hardware graphics accelerators, every pixel, every line, every sprite was drawn manually by the Z80. Developers used integer algorithms for fast pixel coordinate calculations, optimized loops for drawing lines (the Bresenham algorithm, for example, though not directly mentioned, is a prime example of the integer-based approach), and tricks for updating the screen to minimize "attribute clash" (the problem of attribute-based graphics). For instance, remember how they optimized rendering in the "raytracer" for the Spectrum: instead of calculating the color for each of the 64 pixels in an 8×8 block, they traced rays only for the 4 corners, and if the colors matched, they filled the entire block! This resulted in a 16-fold speed increase! And all of this was done with a minimum of multiplications and divisions, which were brutally slow on the Z80.

  • Physics and Collisions – Making Games "Alive"
    Detecting object collisions in games is a critical task. On modern systems, this is solved with complex floating-point algorithms, but the Spectrum had no such luxury. Pixel-perfect collision detection was too slow. Therefore, developers often used a tile-based system: the world was divided into a grid, and collision detection was reduced to checking if two objects were in the same "cell" or "tile." This is an integer operation that was lightning-fast! Even for more complex collisions, such as with sprites, optimizations based on bitwise operations and integer shifts were used to quickly determine overlap.

  • Mathematical Tricks – Squeezing the Most out of the Z80
    Since the Z80 had no hardware multiplication/division, these operations had to be implemented in assembly, often using a series of additions/subtractions and bit shifts. These were purely integer algorithms, written by hand for maximum speed. Even things like calculating sines and cosines for 3D graphics (yes, there were attempts at that on the Spectrum!) were often implemented using lookup tables, where values were pre-calculated and stored as integers or fixed-point numbers.

  • Saving Every Byte – The Art of Compression
    With 48 KB of memory, every byte counted. Developers came up with incredible ways to compress data: graphics, music, levels. This included RLE encoding, the use of palettes, and even "goblin-like" code that looked strange but saved precious bytes.

Friends, this is just the tip of the iceberg! Each of these "integer gems" deserves its own article. In future installments, we will definitely dive deeper into the world of graphics algorithms, figure out how tile-based physics worked, and even try to write our own fast integer operations for the Z80!

From Retro Bytes to Modern Gigabytes: The Legacy of ZX Spectrum

How the Lessons Learned on the "Speccy" are Relevant Today

You might ask, "So what? That’s all history, why do we need this now?" But you’d be wrong, folks! The lessons we learned from squeezing the most out of the "Speccy" are more relevant than ever. The principles of extreme optimization, understanding low-level operations, and the ability to work with limited resources are skills that are worth their weight in gold today.

ZX Spectrum developers didn’t just create programs; they forged a special approach to problem-solving, where efficiency and resource conservation were paramount. It was an approach that required a deep understanding of the hardware and the ability to inventively bypass limitations. Today, when computing resources seem limitless, this "Speccy mindset" is becoming increasingly important in fields like high-performance computing, embedded systems, and, of course, the Internet of Things. It is in these areas, where every processor cycle and every byte of memory matters, that the principles honed on the ZX Spectrum find their direct application.

High-Performance Libraries: Where Low-Level Optimization Matters Today

In the world of high-performance computing, graphics engines (yes, the very ones that render your modern games!), big data processing, and even in financial algorithms where every nanosecond counts, principles similar to those used on the Spectrum are still in use. Many modern libraries for image and video processing, or scientific computing, written in C/C++ or even assembly, use integer arithmetic and fixed-point where floating-point would be too slow or imprecise. Algorithms for binary integer linear programs, for example, are used for efficient resource allocation, and they are designed with a minimum number of passes and no matrix inversion – which is very similar to the Spectrum’s approach to resource conservation.

IoT: When Every Milliwatt Counts

And here, friends, the parallels with the "Speccy" become simply astonishing! Internet of Things (IoT) devices are miniature computers with limited resources: tiny processors, little memory, and most importantly, very strict power consumption requirements. Every operation must be as efficient as possible so that the battery doesn’t die in a couple of hours. This creates a sort of "continuum of resource scarcity," where the same challenges that faced developers on the ZX Spectrum re-emerge in the modern world, demanding similarly inventive solutions.

Examples of Integer Algorithms in IoT:

  • Lightweight Cryptography: Security in IoT is critically important. But traditional encryption algorithms are too "heavy" for tiny IoT devices. This is where lightweight cryptographic algorithms come to the rescue, specifically designed to work with minimal resources, often relying on integer operations. Among such algorithms are AES, XTEA, HIGHT, KLEIN, Piccolo, PRESENT, Serpent, Blowfish, Twofish, ECC, RSA, ML-DSA. Studies show that algorithms like Piccolo, AES, XTEA, and KLEIN are the most energy-efficient, providing maximum device uptime with low power consumption. Symmetric algorithms, such as AES or RC5, are preferable for IoT due to their smaller key sizes and reduced complexity, which leads to faster encryption and lower energy use. Twofish, optimized for 32-bit CPUs, is also an excellent symmetric method. Even ECC (Elliptic Curve Cryptography), though slower than AES, generates short keys and is not computationally intensive, which is important for saving battery and memory. All of these algorithms, at their core, make heavy use of integer arithmetic and bitwise operations, avoiding complex floating-point calculations, which makes them ideal for a resource-constrained world.

  • Algorithms for Sensor Data Processing and Resource Management: Besides cryptography, integer algorithms are indispensable for processing data from various sensors (temperature, humidity, motion), filtering noise, and making real-time decisions when there is no time for complex calculations. Power management, task scheduling, and even the simplest machine learning algorithms on edge devices often use integer-based approaches for maximum efficiency.

To demonstrate these parallels clearly, let’s look at a comparison table:

Limitation Category ZX Spectrum (Approx. Values) Typical IoT Device (Approx. Values) Result/Solution (Integer Algorithms)
CPU Speed 3.5 MHz Z80 Low-frequency microcontroller (few MHz – hundreds of MHz) Manual implementation of multiply/divide, loop optimization
RAM Size 48 KB (16 KB for graphics) Kilobytes/Megabytes (e.g., 64 KB – 512 KB) Fixed-point arithmetic, data compression, efficient memory usage
Hardware Accelerators None (sprites, scrolling) Limited/None (no GPU, specialized units) Tile-based collision system, pixel/bitwise graphics, software effects
Floating-Point Support Poor/Software (in BASIC) Often limited/absent (no FPU) Use of integer arithmetic for all calculations
Power Consumption Not Applicable (stationary power) Critically Important (battery-powered) Lightweight cryptography, algorithms with minimal operations

This table clearly shows that despite decades of progress, the fundamental challenges associated with limited computing resources remain the same. And the solutions that were invented for the "Speccy" continue to inspire and shape development approaches for modern, miniature, yet powerful devices.

Conclusion: Forward, to New Discoveries, While Looking Back at the Past!

And there you have it, folks! Our good old ZX Spectrum, which many consider just a toy from the past, was actually a true laboratory of engineering thought. Its limitations did not slow down progress but, on the contrary, stimulated the emergence of brilliant solutions, many of which were based on the elegance and efficiency of integer algorithms. This is not just nostalgia; it is a recognition that the fundamental principles of optimization, born in conditions of extreme resource scarcity, remain eternally relevant.

We hope this article has sparked your interest in this fascinating topic! Share your memories of the "hacks" you used on the Spectrum, and which games amazed you with their optimization. And, of course, don’t forget to support our community – because it is thanks to enthusiasts like you and me that the history of the "Speccy" continues to live on and inspire new generations of developers.

As promised, in our next articles, we will definitely delve into the details of how these "integer gems" actually worked – from graphics to physics and mathematics on the Z80. Stay tuned, it’s going to be interesting.

A little about support

Friends, I have launched my own Boosty, where you can support my work with a shiny coin. This is a good motivation to write more interesting articles, develop ZX Online and generally do a lot of new things for the ZX Spectrum and the retro platform. (To tell the truth, I will continue to do this in any case, but additional motivation speeds up all the processes 🙂 ).

The post From Bytes to Billions: How the ZX Spectrum Laid the Foundations of Modern Algorithms appeared first on ZX Online - Modern ZX Spectrum Games.

]]>
https://zxonline.net/from-bytes-to-billions-how-the-zx-spectrum-laid-the-foundations-of-modern-algorithms/feed/ 0
ZX Spectrum Graphics Magic: The Basics Every Spectrum Fan Should Know https://zxonline.net/zx-spectrum-graphics-magic-the-basics-every-spectrum-fan-should-know/ https://zxonline.net/zx-spectrum-graphics-magic-the-basics-every-spectrum-fan-should-know/#comments Sun, 15 Jun 2025 02:17:05 +0000 https://zxonline.net/?p=155310 Hey friends! (Or just curious readers peeking under the hood of good old retro games). My previous article on calculating angles with integers was well-received. Several people wrote to me, some asked questions, others offered their own algorithm variations, and it turns out there are quite a few ZX Spectrum programming enthusiasts – it’s fascinating, […]

The post ZX Spectrum Graphics Magic: The Basics Every Spectrum Fan Should Know appeared first on ZX Online - Modern ZX Spectrum Games.

]]>
Hey friends! (Or just curious readers peeking under the hood of good old retro games).

My previous article on calculating angles with integers was well-received. Several people wrote to me, some asked questions, others offered their own algorithm variations, and it turns out there are quite a few ZX Spectrum programming enthusiasts – it’s fascinating, and that’s what all this is for!

In my new article, I wanted to talk in detail about drawing lines and other primitives on the Speccy, but I suddenly realized that some might not be familiar with how the Speccy’s screen is structured at all, and without that, it’s absolutely impossible to move forward. It’s like trying to build a house without knowing what a brick is made of or how to lay it!

So, let’s break down this, frankly, complex topic. We’ll figure out how the classic Spectrum draws things on the screen, and of course, we’ll draw something simple but enlightening! Get your virtual oscilloscopes and soldering irons ready (or just your emulators)!


Part 1: Screen Secrets: Why the Speccy Draws the Way It Does

So, when we turn on our beloved Speccy, we see that magical black window in a white frame, with red lines flashing across it, and then everything turns white with text at the bottom. If we switch the screen background to black (for example, in BASIC with the command "PAPER 0: INK 7"), we’ll see a black field measuring 256 pixels horizontally and 192 pixels vertically. This is our main "canvas" for creativity!

ZX Spectrum Main Screen

It seems simple enough, right? It’s just a pixel grid, like in Paint! If you want to draw a dot at coordinates (X, Y), you just access the "cell" with those coordinates in video memory, put the desired color there, and you’re done.

But here’s the Spectrum’s first trick! Its video memory isn’t as straightforward as you might think. It’s divided into two large parts:

  1. Pixel Area: This stores data on whether each specific pixel is on or off. "On" (value "1") means it’s drawn with the INK color; "off" (value "0") means it’s drawn with the PAPER color. Each pixel is represented by just one bit!

  2. Attribute Area: This stores data on which INK color, which PAPER color, and what effects (BRIGHT/FLASH) will be used for a group of pixels.

The most important point: this "group of pixels" isn’t a single pixel! It’s a block measuring 8 pixels horizontally and 8 pixels vertically. So, the entire 256×192 screen is divided into a grid of such attribute blocks: 256 / 8 = 32 blocks horizontally and 192 / 8 = 24 blocks vertically. A total of 32 * 24 = 768 attribute blocks. In some literature, these "blocks" might be called "characters" or "squares."

For each such block (8×8 pixels), a separate attribute byte is stored in a distinct memory area. This byte contains information:

  • Lower 3 bits (0 to 2): INK color (one of 8 standard colors: 0 = black, 1 = blue, 2 = red, 3 = magenta, 4 = green, 5 = cyan, 6 = yellow, 7 = white).

  • Next 3 bits (3 to 5): PAPER color (also one of the same 8 colors).

  • Bit 6: BRIGHT flag (makes INK and PAPER colors brighter). It’s important to understand that both colors become brighter – both ink and paper – so we can’t draw, for example, dark red on bright white.

  • Bit 7: FLASH flag (makes the INK and PAPER colors of this block constantly swap places). This creates a fun hardware animation – the entire pixel block "flashes" at approximately 2 Hz. This hardware animation takes 0 CPU cycles and (proudly!) was therefore often used in early ZX Spectrum games.

Do you see the catch? Within one 8×8 block, all "on" pixels will be the same INK color, and all "off" pixels will be the same PAPER color, as defined by the attribute byte for that block. You cannot draw one pixel in an attribute block with red INK and a neighboring one with green INK! Both will use the color determined by that block’s attribute.

This leads to the famous "Attribute Clash" – when a moving sprite (image) larger than 8×8 pixels passes through blocks with different attributes, parts of the sprite suddenly change their colors because they fall under the influence of the attributes of the blocks they are in. This is a characteristic feature of the Spectrum that gave games a unique look (and developers a headache!).

So, to draw a colored pixel at coordinates (X, Y) on the Speccy, you need to do two things:

  1. Find the byte in the pixel area of video memory that corresponds to this pixel, and set the appropriate bit.

  2. Find the byte in the attribute area that corresponds to the 8×8 block containing this pixel (its coordinates will be (X/8, Y/8), rounded down), and set the desired INK/PAPER colors and BRIGHT/FLASH flags.

The trickiest part here is the first one! The pixel memory area is not simply laid out row by row. It’s cleverly intertwined to simplify the work of the television display circuit. But we’ll cover that in more detail in the next part.

First, let’s learn how to find the address and the correct bit for one pixel and try to "turn it on."


Part 2: Lighting a Star (Drawing a Dot)!

Imagine you want to draw just a single dot on the Speccy screen. Let’s say it’s at coordinates X=50, Y=50.

At first glance, it seems: well, the screen is 256×192. Each row is 256 pixels. Each pixel is 1 bit. So, a row occupies 256 / 8 = 32 bytes. The screen is 192 rows * 32 bytes/row = 6144 bytes. The address of point (X, Y) should be start_address + Y * 32 + X / 8, and the required bit in that byte is X % 8. Logical, right?

Not quite! The ZX Spectrum wouldn’t be itself if everything were so simple! Its video memory (pixel part) is structured very specifically to simplify the operation of the hardware video controller ULA (which, in essence, is very primitive there). Screen memory starts at address $4000 (16384 decimal). And from there, it doesn’t go sequentially row by row!

The address of the byte containing pixel (X, Y) is calculated by a rather tricky formula, but before I write it down, let’s do a thought experiment.

Imagine you’re moving from address $4000 downwards and filling all the pixels behind you (writing the value $FF to all traversed bytes). For the first 31 bytes, you’ll see the zero row filled from left to right. But as soon as you go beyond the right edge, you won’t land on row 1, but on row 8! That’s how an offset of +32 corresponds to row 8. If you continue, after another 32 bytes, you’ll appear on row 16. And so on, up to row 64 (in total, you’ll have to go through 256 (or $100) bytes. But contrary to expectations, you’ll then find yourself on row 1!

Wow! It turns out that to move down one row, we need to add +$100 to the address. It’s unknown why Sir Clive Sinclair chose this particular order, but it turned out to be very convenient for displaying simple characters that occupy exactly one pixel block! After all, increasing a two-byte register by $100 only requires one short assembly instruction, like INC H if the address is stored in the HL register.

This way, we’ll go through $800 bytes (2048 of them) and fill exactly one-third of the screen – the top 64 rows.

And only after that will we start filling the second third, and then the third third.

Does this remind you of anything? Yes, yes, we all remember how the splash screen of any game loads from cassette – with strange lines. That’s exactly it!

So, we can schematically draw the formula for calculating the byte address.

Screen Address Calculation for Pixel Draw on ZX Spectrum

It shows that to shift down by 1 line, you need to add $0100; to shift by 8 lines, add $0020; and to shift by 64 lines, add $0800.

Here’s what the formula will look like in Javascript (I’ll use Javascript to prototype our ideas because it’s very simple and can be run right in the browser!):

Adr = 0x4000 + (Y & 0xc0 << 5) + (Y & 0x38 << 2) + (Y & 0x07 << 8) + (X & 0xF8 >> 3)

Well, by old tradition, let’s write the code for Z80 Asm too!

; Let X be in register L, and Y in register H.
; We'll assume that the value of Y is already within the valid range (0 to 191)
; and won't check it for speed.
; On exit, the address will be in HL.
; The procedure uses and "corrupts" registers A and B.
GetScrAddr
    ld a, $38
    and h   ; extract a small 3-bit piece from Y
    rlca
    rlca      ; shift it left by 2 bits
    ld b, a  ; and temporarily save it in B
    ld a, $f8  ; extract the upper 5 bits from X
    and l
    rrca       ; and shift it 3 bits to the right
    rrca
    rrca
    or b    ; combine with B
    ld l, a  ; L calculation is complete! Now calculate H
    ld a, $c0
    and h  ; extract the 2 most significant bits from Y
    rrca
    rrca
    rrca    ; shift them 3 bits to the right
    ld b, a  ; save temporarily in B
    ld a, $07 ; extract the 3 least significant bits from Y
    and h
    or b   ; no need to shift them - they are already in place. Just combine them with B
    or $40  ; and append $40 to get the address $4000-$57ff
    ld h, a   ; H is ready!
    ret

Phew! We exhaled. Now we know the byte address. But that’s not enough! This byte contains 8 pixels. Which one is ours? The desired pixel (X, Y) is bit number X % 8 (remainder of division by 8) (or X & $07) in that byte. Furthermore, bit 7 is the leftmost of the eight pixels, bit 0 is the rightmost. So, we need bit number 7 - (X & $07).

All that’s left is to "turn on" this bit, i.e., set it to 1, without touching the other 7 pixels in this byte. To do this, we read the byte from video memory, perform a bitwise OR (OR) operation with a mask in which only our desired bit is set, and write the result back. The mask for bit b is 1 << b. So, for bit 7 - (X & $07), the mask will be 1 << (7 - (X & $07)).

Now let’s try to calculate the address for X=50, Y=50 by hand:

  • Y = 50. In binary: 00110010.
  • X = 50. In binary: 00110010.

Byte address calculation:

  • Y & $C0 (00110010 & 11000000) = 00000000. Shift << 5 = 0.
  • Y & $38 (00110010 & 00111000) = 00110000 (0x30). Shift << 2 = 11000000 (0xc0).
  • Y & $07 (00110010 & 00000111) = 00000010 (2). Shift << 8 = 10 0000000 (0x0200).
  • X >> 3 (00110010 >> 3) = 00000110 (6).

Summing it all up, we get the address = $4000 (16384) + 0 + 192 + 512 + 6 = 16384 + 710 = 17094 ($42C6).

Required bit calculation:

  • X & $07 (00110010 & 00000111) = 00000010 (2).
  • Required bit = 7 - 2 = 5.
  • Mask for bit 5: 1 << 5 = 00100000 ($20).

Now, to draw the point (50, 50), we need to:

  1. Read the byte at address $42C6 – but we can skip this step for simplicity if we have an empty screen.
  2. Perform a bitwise OR with the mask $20 (to set bit 5).
  3. Write the modified byte back to address $42C6.

What about the color? We haven’t set the color yet! For that, we need to access the attribute area.

The address of the attribute byte for point (X, Y) is much simpler to calculate:

Attribute_Address = $5800 + (Y >> 3) * 32 + (X >> 3)

For X=50, Y=50:

  • Y / 8 (50 / 8 = 6 with remainder) = 6.
  • X / 8 (50 / 8 = 6 with remainder) = 6.

Attribute address = $5800 (22528) + 6 * 32 + 6 = 22528 + 192 + 6 = 22726 ($58C6).

At address $58C6 lies the attribute byte for the 8×8 pixel block that contains point (50, 50). If we want our point to be drawn bright red (BRIGHT=1, INK=2), and the background to be black (PAPER=0), this byte should be:

BRIGHT=1 (bit 6), FLASH=0 (bit 7), PAPER=0 (bits 3-5), INK=2 (bits 0-2).
Binary: 0 1 000 010 = %01000010 = $42.

So, to draw a bright red point (50, 50) on a black background:

  1. At address $58C6, write byte $42.
  2. At address $42C6, read the byte, perform OR with $20, write back.

And there’s our dot! Look at our drawing below: there’s a black square, and inside it, a tiny red dot!

You can view the code and experiment with it by clicking the icon in the corner of this drawing and then selecting "CODE".


Part 3: Bringing Bytes to Life: Drawing a Funny 8×8 Face

So, in the last part, we fought our way to understanding how to find the address of a single pixel in the Speccy’s tricky video memory and how to "light it up" by setting the right bit in the right byte at the right address. Phew! But an image isn’t just one dot; it’s many, many dots!

The most basic graphical primitive after a dot is, essentially, a character. On the Spectrum, characters (letters, numbers, symbols) have a standard size of 8 pixels wide and 8 pixels high. How are they stored? Very simply: each line of such a character is one byte!

Why? Because 8 pixels are exactly 8 bits. And a byte is 8 bits! So, the byte 10110010 means that in this character line, pixels at positions 7, 5, 4, and 1 are "on" (INK) (counting from left to right, from 7 to 0), and the rest are "off" (PAPER).

Bit: 7 6 5 4 3 2 1 0 (pixel position from left to right)
Byte: 1 0 1 1 0 0 1 0  ->  # . # # . . # .

So, our 8×8 character is stored as a sequence of eight bytes. The first byte is the top line of the character, the second is the next, and so on, until the eighth byte – the very bottom line.

We want to draw a funny face. Here’s its "matrix" of eight bytes:
$3c, $42, $81, $a5, $81, $99, $42, $3c

(Some people memorize poems and capital city names by heart; we, Speccy enthusiasts, have a special gift – we memorize numbers and codes. For example, I remember the exact bytes for drawing this face since 1993.)

Let’s convert these bytes to binary and see what they look like if we represent 1 as # (INK color) and 0 as . (PAPER color):

  • 0x3c = 00111100 -> ..####..
  • 0x42 = 01000010 -> .#....#.
  • 0x81 = 10000001 -> #......#
  • 0xa5 = 10100101 -> #.#..#.#
  • 0x81 = 10000001 -> #......#
  • 0x99 = 10011001 -> #..##..#
  • 0x42 = 01000010 -> .#....#.
  • 0x3c = 00111100 -> ..####..

How do you draw such an 8×8 block on the screen, starting at coordinates (X, Y)?

We know how to find the byte address for pixel (X, Y). Let’s call it Base address. This is the address of the byte that will contain the first row of our face (0x3c). But where are the bytes for the subsequent rows (Y+1, Y+2, …, Y+7)?

As we found out in the last part, due to the Speccy’s tricky video memory arrangement, this is not true! The byte addresses for consecutive rows (within one 8-row Y-block) are 256 bytes apart.

(Of course, this is true as long as Y and Y+7 are within the same 8-row block along the Y-axis, i.e., Y % 8 + 7 < 8. But for drawing an 8×8 character, we usually choose Y such that Y is a multiple of 8, for example Y=0, 8, 16… In this case, the entire character fits precisely into one such 8-row Y-block, and this 256-byte step rule applies to all 8 rows of the character).

Besides pixel bytes, we naturally also need an attribute! Our entire 8×8 character fits perfectly into one attribute block. This means all pixels of this face will use the same INK/PAPER colors and the same BRIGHT/FLASH flags that we write into the attribute byte for the block containing (X, Y). We also know how to calculate the address of this attribute byte from the previous part: $5800 + (Y / 8) * 32 + (X / 8).

So, the algorithm for drawing an 8×8 character/face at coordinates (X, Y) is as follows:

  1. Choose the desired attribute byte (e.g., blue INK on white PAPER = %0 0 111 001 = $39).
  2. Calculate the attribute byte address for block (X, Y): AttrAddress(X, Y).
  3. Write the chosen attribute byte to address AttrAddress(X, Y).
  4. Calculate the base pixel byte address for (X, Y): for this, we will use X and Y rounded to multiples of 8, e.g., X = 48 and Y = 48.
  5. Write the 8 bytes of our image sequentially, incrementing the address by $100.

Let’s look at the code.

// This is the array where we store the "graphic data" of our smiley
const smiley_data = [
  0x3c, 0x42, 0x81, 0xa5, 0x81, 0x99, 0x42, 0x3c
];

let adr = 0x40c6;  // This is the address of the top (zero) byte of the location where we intend to output the smiley
for (let i = 0; i < 8; i ++) {
    ram[adr] = smiley_data[i];   // In a loop, transfer data from the array to video memory 8 times
    adr = adr + 0x100;   // move to the next line
}

ram[0x58c6] = 0x39;  // Attribute address for our face and its color

And traditionally, the Asm code:

DrawSmile:
    ld hl, $40c6     ; HL - address in video RAM
    ld de, .smile_data   ; DE points to the smiley data array
    ld b, 8    ; B will count down from 8
.loop
    ld a, (de)    ; get byte from DE
    ld (hl), a     ; and put it into HL
    inc de        ; move to the next smiley byte
    inc h          ; move down a line on the screen (this is equivalent to HL = HL + $0100, only faster, because L doesn't change, and H increments by 1)
    djnz .loop  ; this loops our program 8 times, to repeat the same for all 8 bytes
    ret
.smile_data
    db $3c, $42, $81, $a5, $81, $99, $42, $3c

So, we’ve learned how to take 8 bytes of data representing an 8×8 pixel image, find the correct attribute block, write the color there, and most importantly, write these 8 bytes to the correct locations in pixel memory, accounting for the 256-byte step between lines. This is a very important skill for anyone who wants to draw anything more complex than a single dot on the Spectrum!

In the next part, we’ll delve into how to optimize pixel address calculation so that we don’t have to perform all those complex bitwise operations "from scratch" every time.


Part 4: Breaking Through Memory: Optimizing Address Calculation

We’ve learned how to find the address of a single byte of pixel memory for point (X, Y), the address of the attribute byte for block (X, Y), and even how to write 8 bytes for a character, knowing that rows are 256 bytes apart. That’s great! But if we look closely at the pixel byte address calculation formula:

Address = $4000 + ((Y & $C0) << 5) + ((Y & $38) << 2) + ((Y & $07) << 8) + (X >> 3)

It’s clear that it’s quite cumbersome. Every time, for each point, for each primitive byte (line, sprite), performing all these bitwise operations, shifts, and additions on the Z80 will consume precious CPU time. If we want to draw fast-moving objects or many details, we need something quicker!

As usual, there are several optimization paths.

Method 1: Moving Gradually (Incremental Calculation)

What if, instead of calculating the address from scratch $4000 every time, we move from an already known address to an adjacent one? We’ve already figured out how to move down one line – just add $100! But this won’t work everywhere. When we reach the 7th line in an 8*8 pixel block, we’ll have to go back $700 (7 lines up) and then increase the address by $20! But let’s start from simple to complex.

  • Shift pixel right (+1 to X): If it’s about outputting a single pixel, we simply shift the byte mask by 1 bit to the right. For example, if we had a mask $20 for coordinate X=50, to print a point with coordinates X=51, we just need to rotate the byte 1 bit to the right; the video RAM address doesn’t even change! However, if our point is already pressed against the right edge of the byte (value $01), then we’ll still have to increment the address by 1. But that’s so simple!

  • Shift pixel left (-1 to X): This is completely analogous to shifting right, but in reverse. If the bit is not pressed against the left edge (value not $80), then shift the byte left by 1 bit. Otherwise, decrement the video memory address.

  • Shift line down (+1 to Y): This is where the difficulties begin! If we are at Addr(X, Y) and want to go to Addr(X, Y+1). We already know that bytes of consecutive rows within one 8-row block are 256 bytes apart. So, if Y % 8 is not equal to 7, the address of the next row is simply Addr(X, Y) + 256! This is a fast addition on the Z80 (ADD HL, $0100).
    But if Y % 8 is equal to 7 (we are in the last row of an 8-row block), moving to Y+1 means crossing a block boundary. And here, the offset is no longer 256! It depends on which block and which "third" of the screen we were in. Calculating the address for (X, Y+1) after crossing the boundary will require either a complete recalculation of the address from scratch using the complex formula, or another trick.

  • Shift line up (-1 to Y): Similarly, if Y % 8 is not equal to 0, the address of the previous row is Addr(X, Y) - 256. If Y % 8 is equal to 0 (we are in the first row of an 8-row block), we cross the boundary again and a complete recalculation or trick is needed. I note that moving up when drawing graphics on the ZX Spectrum is rarely used, but it also has its place.

Here’s the trick used. We can check the current address and, based on it, deduce how much to add to the address. This algorithm is also widely known as the "LINEDOWN_HL Algorithm".

; Input: HL = Current address from which we want to move to the next line
; Output: HL = Address of the next line on the screen
; Uses: register A.
LineDown:
    inc h    ; try moving down 1 line (HL = HL + $0100 conceptually)
    ld a, h
    and 7    ; check that we haven't jumped over an 8x8 block boundary (e.g., was $47xx, became $48xx)
    ret nz    ; if the lower 3 bits are not zero, then everything is within the block, finished.

    ld a, l    ; if yes, then we need to correct, because we clearly landed in the wrong place
    add a, 32    ; shift by 32 bytes (to land on the next line within the screen segment) - e.g., was $48e5, became $4805 with a carry
    ld l, a
    ret c    ; if an overflow occurred during summation, it means we correctly landed in the next third ($4805 - that's exactly the case)

    ld a, h    ; Well, if not, it means we overshot and need to correct H - return to the previous third (e.g., was $47c5, mistakenly
    sub 8    ; first landed in $48c5, then in $48e5 - no carry.
    ld h, a   ; Decrease H by 8 to get $40e5 - the correct address.

    ret

But all this takes time. Calculating the initial address, then moving down to draw each byte… can’t it be faster?

Yes, it can! And SIGNIFICANTLY FASTER.

Method 2: All-Purpose Reference (Table Method)

Probably the most popular way to quickly find the address for any coordinates (X, Y) is to use a pre-calculated table.

  • Row Address Table: Create a table in memory (an array of 16-bit words) that stores the address of the first byte (X=0) for each Y (from 0 to 191). The table will have 192 elements, each 2 bytes long (an address occupies 16 bits). Total size: 192 * 2 = 384 bytes.
    How to get the address for (X, Y) with such a table?

    1. Take Y.
    2. Use Y as an index for our RowStartTable. Get base_address = RowStartTable[Y]. This is the address of the byte for (0, Y).
    3. Add the X offset: Address = base_address + (X >> 3) (X / 8).

Accessing the table (loading a 16-bit value by index) and one 16-bit addition is very fast on the Z80! This is arguably the fastest way to get the pixel byte address for arbitrary coordinates (X, Y).

We won’t provide the table for calculating the address in the attribute area here; you’ve probably understood the logic and can create it by analogy. To be fair, attribute addresses are much simpler to calculate and are usually done without tables, just with register shifts.

Tables consume memory but offer a huge speed advantage when frequent access to arbitrary coordinates is needed.

Let’s write the code for calculating the address using the table method (for example, for our pixel from the first part) and check its operation!

// The most important thing is this table!
const scr_adr = [
  0x4000, 0x4100, 0x4200, 0x4300, 0x4400, 0x4500, 0x4600, 0x4700,
  0x4020, 0x4120, 0x4220, 0x4320, 0x4420, 0x4520, 0x4620, 0x4720,
  0x4040, 0x4140, 0x4240, 0x4340, 0x4440, 0x4540, 0x4640, 0x4740,
  0x4060, 0x4160, 0x4260, 0x4360, 0x4460, 0x4560, 0x4660, 0x4760,
  0x4080, 0x4180, 0x4280, 0x4380, 0x4480, 0x4580, 0x4680, 0x4780,
  0x40a0, 0x41a0, 0x42a0, 0x43a0, 0x44a0, 0x45a0, 0x46a0, 0x47a0,
  0x40c0, 0x41c0, 0x42c0, 0x43c0, 0x44c0, 0x45c0, 0x46c0, 0x47c0,
  0x40e0, 0x41e0, 0x42e0, 0x43e0, 0x44e0, 0x45e0, 0x46e0, 0x47e0,

  0x4800, 0x4900, 0x4a00, 0x4b00, 0x4c00, 0x4d00, 0x4e00, 0x4f00,
  0x4820, 0x4920, 0x4a20, 0x4b20, 0x4c20, 0x4d20, 0x4e20, 0x4f20,
  0x4840, 0x4940, 0x4a40, 0x4b40, 0x4c40, 0x4d40, 0x4e40, 0x4f40,
  0x4860, 0x4960, 0x4a60, 0x4b60, 0x4c60, 0x4d60, 0x4e60, 0x4f60,
  0x4880, 0x4980, 0x4a80, 0x4b80, 0x4c80, 0x4d80, 0x4e80, 0x4f80,
  0x48a0, 0x49a0, 0x4aa0, 0x4ba0, 0x4ca0, 0x4da0, 0x4ea0, 0x4fa0,
  0x48c0, 0x49c0, 0x4ac0, 0x4bc0, 0x4cc0, 0x4dc0, 0x4ec0, 0x4fc0,
  0x48e0, 0x49e0, 0x4ae0, 0x4be0, 0x4ce0, 0x4de0, 0x4ee0, 0x4fe0,

  0x5000, 0x5100, 0x5200, 0x5300, 0x5400, 0x5500, 0x5600, 0x5700,
  0x5020, 0x5120, 0x5220, 0x5320, 0x5420, 0x5520, 0x5620, 0x5720,
  0x5040, 0x5140, 0x5240, 0x5340, 0x5440, 0x5540, 0x5640, 0x5740,
  0x5060, 0x5160, 0x5260, 0x5360, 0x5460, 0x5560, 0x5660, 0x5760,
  0x5080, 0x5180, 0x5280, 0x5380, 0x5480, 0x5580, 0x5680, 0x5780,
  0x50a0, 0x51a0, 0x52a0, 0x53a0, 0x54a0, 0x55a0, 0x56a0, 0x57a0,
  0x50c0, 0x51c0, 0x52c0, 0x53c0, 0x54c0, 0x55c0, 0x56c0, 0x57c0,
  0x50e0, 0x51e0, 0x52e0, 0x53e0, 0x54e0, 0x55e0, 0x56e0, 0x57e0,  
];

// And this is the actual address calculation!
let x = 50;
let y = 50;

let adr = scr_adr[y] + ((x & 0xF8) >> 3);

// That's it! So simple!

But the most interesting part is the assembly implementation!

; Conditions are exactly the same as in the first version - on entry H = Y, L = X,
; On exit, HL will contain the address.
; The procedure corrupts registers DE and A.
GetScrAdr_Table:
    ld a, l          ; Get X coordinate into A
    and $F8          ; Mask out lower 3 bits of X
    rrca             ; Shift right 3 times (X / 8)
    rrca
    rrca
    ld c, a          ; Store X / 8 in C (low byte of byte offset)
    ld b, 0          ; Clear B (high byte of byte offset, so BC = X / 8)

    ld a, h          ; Get Y coordinate into A
    ld l, a          ; Put Y into L for indexing
    ld h, 0          ; Clear H, so HL = Y (as a 16-bit index)
    add hl, hl       ; Multiply HL by 2 (Y * 2), because each table entry is a WORD (2 bytes)
    ld de, .scrtab   ; Load address of table start into DE
    add hl, de       ; Add table start to index: HL now points to the WORD at .scrtab[Y*2]

    ld e, (hl)       ; Get low byte of screen address for Y into E
    inc hl           ; Point to next byte (high byte)
    ld d, (hl)       ; Get high byte of screen address for Y into D
    ; Now DE holds the base screen address for row Y (i.e., address of pixel (0, Y))

    ex de, hl        ; Swap DE and HL, so HL now holds the base screen address for row Y
    add hl, bc       ; Add the X/8 offset (stored in BC) to HL
    ret

.scrtab
   ; here the table from our JS example fits
   dw 0x4000, 0x4100, 0x4200, 0x4300, 0x4400, 0x4500, 0x4600, 0x4700
   dw 0x4020, 0x4120, 0x4220, 0x4320, 0x4420, 0x4520, 0x4620, 0x4720
   dw 0x4040, 0x4140, 0x4240, 0x4340, 0x4440, 0x4540, 0x4640, 0x4740
   dw 0x4060, 0x4160, 0x4260, 0x4360, 0x4460, 0x4560, 0x4660, 0x4760
   dw 0x4080, 0x4180, 0x4280, 0x4380, 0x4480, 0x4580, 0x4680, 0x4780
   dw 0x40a0, 0x41a0, 0x42a0, 0x43a0, 0x44a0, 0x45a0, 0x46a0, 0x47a0
   dw 0x40c0, 0x41c0, 0x42c0, 0x43c0, 0x44c0, 0x45c0, 0x46c0, 0x47c0
   dw 0x40e0, 0x41e0, 0x42e0, 0x43e0, 0x44e0, 0x45e0, 0x46e0, 0x47e0

   dw 0x4800, 0x4900, 0x4a00, 0x4b00, 0x4c00, 0x4d00, 0x4e00, 0x4f00
   dw 0x4820, 0x4920, 0x4a20, 0x4b20, 0x4c20, 0x4d20, 0x4e20, 0x4f20
   dw 0x4840, 0x4940, 0x4a40, 0x4b40, 0x4c40, 0x4d40, 0x4e40, 0x4f40
   dw 0x4860, 0x4960, 0x4a60, 0x4b60, 0x4c60, 0x4d60, 0x4e60, 0x4f60
   dw 0x4880, 0x4980, 0x4a80, 0x4b80, 0x4c80, 0x4d80, 0x4e80, 0x4f80
   dw 0x48a0, 0x49a0, 0x4aa0, 0x4ba0, 0x4ca0, 0x4da0, 0x4ea0, 0x4fa0
   dw 0x48c0, 0x49c0, 0x4ac0, 0x4bc0, 0x4cc0, 0x4dc0, 0x4ec0, 0x4fc0
   dw 0x48e0, 0x49e0, 0x4ae0, 0x4be0, 0x4ce0, 0x4de0, 0x4ee0, 0x4fe0

   dw 0x5000, 0x5100, 0x5200, 0x5300, 0x5400, 0x5500, 0x5600, 0x5700
   dw 0x5020, 0x5120, 0x5220, 0x5320, 0x5420, 0x5520, 0x5620, 0x5720
   dw 0x5040, 0x5140, 0x5240, 0x5340, 0x5440, 0x5540, 0x5640, 0x5740
   dw 0x5060, 0x5160, 0x5260, 0x5360, 0x5460, 0x5560, 0x5660, 0x5760
   dw 0x5080, 0x5180, 0x5280, 0x5380, 0x5480, 0x5580, 0x5680, 0x5780
   dw 0x50a0, 0x51a0, 0x52a0, 0x53a0, 0x54a0, 0x55a0, 0x56a0, 0x57a0
   dw 0x50c0, 0x51c0, 0x52c0, 0x53c0, 0x54c0, 0x55c0, 0x56c0, 0x57c0
   dw 0x50e0, 0x51e0, 0x52e0, 0x53e0, 0x54e0, 0x55e0, 0x56e0, 0x57e0 

But there are other solution variants with tables for Z80 Asm too! For example, you can arrange the table differently and thus save a few cycles on address calculation.

; Conditions are exactly the same as in the first version - on entry H = Y, L = X,
; On exit, HL will contain the address.
; The procedure corrupts registers DE and A.
GetScrAdr_Table:
    ex de, hl    ; now D = Y, E = X
    ld l, d        ; we write the low byte of the table address - the offset - into L
    ld h, HIGH(.scrtab)  ; and the high byte of the table address into H
    ld a, $f8
    and e
    rrca
    rrca
    rrca
    or (hl)   ; Get the low byte of the address and immediately combine it with the part derived from X
    inc h      ; move to the second half of the table, where the high bytes of the address are stored
    ld h, (hl)  ; get the high byte of the address and immediately write it into place - into register H
    ld l, a    ; Write the calculated offset into L
    ret 

   align 256  ; this guarantees that the table will be placed at an address multiple of 256 - this is important for us
.scrtab
    ; low halves of addresses
    db $00, $00, $00, $00, $00, $00, $00, $00, $20, $20, $20, $20, $20, $20, $20, $20
    db $40, $40, $40, $40, $40, $40, $40, $40, $60, $60, $60, $60, $60, $60, $60, $60
    db $80, $80, $80, $80, $80, $80, $80, $80, $a0, $a0, $a0, $a0, $a0, $a0, $a0, $a0
    db $c0, $c0, $c0, $c0, $c0, $c0, $c0, $c0, $e0, $e0, $e0, $e0, $e0, $e0, $e0, $e0

    db $00, $00, $00, $00, $00, $00, $00, $00, $20, $20, $20, $20, $20, $20, $20, $20
    db $40, $40, $40, $40, $40, $40, $40, $40, $60, $60, $60, $60, $60, $60, $60, $60
    db $80, $80, $80, $80, $80, $80, $80, $80, $a0, $a0, $a0, $a0, $a0, $a0, $a0, $a0
    db $c0, $c0, $c0, $c0, $c0, $c0, $c0, $c0, $e0, $e0, $e0, $e0, $e0, $e0, $e0, $e0

    db $00, $00, $00, $00, $00, $00, $00, $00, $20, $20, $20, $20, $20, $20, $20, $20
    db $40, $40, $40, $40, $40, $40, $40, $40, $60, $60, $60, $60, $60, $60, $60, $60
    db $80, $80, $80, $80, $80, $80, $80, $80, $a0, $a0, $a0, $a0, $a0, $a0, $a0, $a0
    db $c0, $c0, $c0, $c0, $c0, $c0, $c0, $c0, $e0, $e0, $e0, $e0, $e0, $e0, $e0, $e0

   db 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0     ; fill data for offsets 192 and above with zeros,
   db 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0     ; because nothing should be displayed when Y >= 192
   db 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
   db 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

   ; now - high halves of addresses
   db $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40
   db $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40
   db $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40
   db $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40, $40

   db $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48
   db $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48
   db $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48
   db $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48, $48

   db $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50
   db $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50
   db $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50
   db $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50, $50

   db 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0     ; fill data for offsets 192 and above with zeros,
   db 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0     ; because nothing should be displayed when Y >= 192
   db 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
   db 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0

Another advantage of the table method is that addresses in the table follow each other sequentially, so we eliminate the need to use algorithms like LineDown for moving to the next line. Once we’ve calculated an address in the table, we simply take addresses from it one after another, and it turns out we’re moving down, line by line!

; Assume we need to display a very tall object, taller than 8 pixels (e.g., 24)
; Then the algorithm could look something like this
; H = Y, L = X - as usual.
DrawTallObject:
    ld a, l    ; Get X coordinate into A
    and $F8    ; Mask out lower 3 bits of X
    rrca       ; Shift right 3 times (X / 8)
    rrca
    rrca
    ld c, a    ; Store X / 8 in C (low byte of byte offset)
    ld b, 0    ; Clear B, so BC = X / 8 (constant offset for X)

    ld a, h    ; Get Y coordinate into A
    ld l, a    ; Put Y into L for indexing
    ld h, 0    ; Clear H, so HL = Y (as a 16-bit index)
    add hl, hl ; Multiply HL by 2 (Y * 2), because each table entry is a WORD (2 bytes)
    ld de, .scrtab ; Load address of table start into DE
    add hl, de ; Add table start to index: HL now points to the WORD at .scrtab[Y*2]
    ex de, hl  ; Store the table pointer in DE, HL will hold the row start address

; Now HL holds the pointer to the .scrtab entry for the current Y.
; We can loop, reading addresses from the table and drawing rows.

.draw_loop:
    ld e, (de) ; Get low byte of screen address for Y into E
    inc de     ; Point to next byte (high byte)
    ld d, (de) ; Get high byte of screen address for Y into D
    inc de     ; Point DE to the next Y's entry in .scrtab
    ex de, hl  ; Swap DE and HL, so HL now holds the base screen address for row Y
    add hl, bc ; Add the X/8 offset (stored in BC) to HL
    ; Now in HL we have the correct byte address for the current row of the object

    ; Output the object's row data here.
    ; For example, if we assume the object data is in (IX) and it's 1 byte wide:
    ; ld a, (ix)
    ; ld (hl), a
    ; inc ix ; move to next byte of object data

    ex de, hl  ; Return the table pointer to DE for the next iteration

    jp/jr/djnz .draw_loop  ; looping here
    ret

In general, there are many, many algorithms for displaying graphics on the Spectrum. And we will definitely try to cover them in future articles.

Comparison of Methods:

  • Direct Formula Calculation: Significantly slower than table lookup. Does not require memory for tables (apart from $4000). Good if there’s no memory for tables, but arbitrary access is needed.

  • Table-based: The fastest way to get an address for any arbitrary coordinates (X, Y) or (X, Y/8). Requires a minimum of 384 bytes for pixel rows or 48 bytes for attribute rows.

In practice, games often use a combination: tables for quickly getting the start of a row or block, and then incremental methods for movement within a row or block.

Understanding all these address calculation methods is key to fast and efficient graphics on the ZX Spectrum! Knowing how to find the right byte and the right bit allows you to move on to more complex things – drawing lines, circles, and, of course, sprites!


Conclusion and Takeaways

Well, friends! We’ve covered a significant journey in this post. From an emotional introduction about the magic of retro games and the challenges of the Z80, we delved right into the heart – or rather, the "brains" – of the ZX Spectrum’s graphics subsystem!

We learned that the Speccy screen is not just a uniform pixel grid, but a clever combination of two memory areas: the pixel area, which tells whether a pixel is lit or not, and the attribute area, which defines colors (INK/PAPER) and effects (BRIGHT/FLASH) for entire 8×8 pixel blocks. We understood why the famous "attribute clash" occurs and found out it’s not a bug, it’s a feature!

Then, armed with this knowledge, we stormed address arithmetic. We understood (or at least saw) how challenging it is to calculate the address of just one pixel from its (X, Y) coordinates due to the peculiar arrangement of data in pixel memory. But we found both formulas, JS code, and even approximate Z80 code that shows how to do it.

From a single dot, we moved on to the 8×8 character – the basic building block of many retro games. We understood how character data (8 bytes, each describing a line of 8 pixels) is laid out in video memory, and that bytes of consecutive character lines (within an 8-line Y-block) are 256 bytes apart.

And finally, we peered into the arsenal of retro-optimizations, examining different methods for fast address calculation: from incremental shifts (good for movements within a block) to high-speed table lookup (excellent for arbitrary access) and clever direct bit gymnastics in assembly.

Understanding how the Speccy screen is structured, how pixels and colors reside in memory, and how to quickly find the right addresses – this is the absolute foundation for any graphics on this platform. Without it, you can’t draw lines efficiently, move sprites quickly, or create beautiful backgrounds.

This knowledge is your key to unlocking the Speccy’s potential. It’s part of that "magic" where, knowing the intricacies of the hardware, you can make it do things that seem impossible at first glance. And even if pixel address calculation seems daunting, once you master it, you’ll feel like a true byte overlord!

I truly hope this article has "enlightened" you on the ZX Spectrum’s graphics architecture and given you food for thought. The best thing you can do now is to grab an emulator or real hardware, take the provided code examples (especially the assembly one, after adapting it to your favorite assembler!), and try it yourself! Draw a dot, draw a face, try drawing them in different places, different colors. Play with attribute bits. See how it looks. This is the best way to reinforce the material.

And, of course, don’t hesitate to share your successes, difficulties, questions, or perhaps your own even cooler ways of calculating addresses in the comments below this article! Knowledge sharing is exactly what drives our passion for retro-programming.

Thank you so much for reading to the end and sharing this dive into the world of ZX Spectrum graphics with me! Until next time!

With love for Speccy and pixels,

The post ZX Spectrum Graphics Magic: The Basics Every Spectrum Fan Should Know appeared first on ZX Online - Modern ZX Spectrum Games.

]]>
https://zxonline.net/zx-spectrum-graphics-magic-the-basics-every-spectrum-fan-should-know/feed/ 4
Angle Calculation on Z80: Doing the Impossible at 3.5 MHz https://zxonline.net/angle-calculation-on-z80-doing-the-impossible-at-3-5-mhz/ https://zxonline.net/angle-calculation-on-z80-doing-the-impossible-at-3-5-mhz/#comments Mon, 26 May 2025 14:33:08 +0000 https://zxonline.net/?p=155267 Hello, friends! (Or just readers interested in peeking under the hood of good old retro games). How often do we recall that magical time when every kilobyte, every Z80 clock cycle on our beloved Speccy counted? We squeezed the most out of those 3.5 MHz to make magic happen on screen – sprites moved, bombs […]

The post Angle Calculation on Z80: Doing the Impossible at 3.5 MHz appeared first on ZX Online - Modern ZX Spectrum Games.

]]>
Hello, friends! (Or just readers interested in peeking under the hood of good old retro games). How often do we recall that magical time when every kilobyte, every Z80 clock cycle on our beloved Speccy counted? We squeezed the most out of those 3.5 MHz to make magic happen on screen – sprites moved, bombs exploded, enemies ran…

But what does it take for all of this to move not just randomly, but smartly? For instance, for a bullet to fly precisely to its target? Or for an enemy to follow the shortest path to the player? That’s right – you need to know the direction. And direction is, essentially, an angle!

But here’s the catch! Our good old Z80 is an integer processor. No fancy co-processors, no native floating-point operations, sines, cosines, or, dare I say, arctangents! Calculating the angle between two points (or a vector (dx, dy) – the coordinate difference) is no simple task for it. It’s not LD A,B or ADD HL,DE. It’s something from higher mathematics that, it would seem, couldn’t be handled at 3.5 MHz without slowdowns.

That’s why old games often employed clever tricks, workarounds, and simplifications. Because every extra millisecond spent on calculations means a drop in framerate, less responsive controls, or simply a boring game because enemies are dumb and can’t really find you.

But what if I told you that you can get a sufficiently accurate angle value for game purposes using only integer arithmetic and small lookup tables? Without slowdowns and with an elegance worthy of a true assembly master! That’s exactly what we’ll discuss today. We’ll break down the simplest (and roughest) approaches, hint at the complexities of more direct ones, and then I’ll show you how to apply a little math (but don’t worry, just a tiny bit!) and tables to make our Z80 calculate angles quickly and efficiently.

Get ready to dive into the world of integer magic!


Fast, but Crude… or Direct, but Slow?

Angle Definition

When you need to find the angle of a vector (dx, dy) on the Z80, the first thought is "something simpler". The simplest is to look at the signs of dx and dy and determine which quadrant the angle is in. Positive dx and dy? That means down-right (for Speccy’s coordinate system). Negative dx and positive dy? Down-left. This is instantaneous but provides accuracy within a 90-degree range, meaning +/- 45 degrees. For many tasks, this isn’t enough. You can refine it by comparing the absolute values of dx and dy – this will yield 8 directions (45 degrees each). Fast, but still crude – like trying to aim while looking through a very blurry binocular.

Quadrants

What about the "direct" approach – angle = atan(dy / dx)? Arctangent is needed! On a PC, you’d just use atan2(x, y), and you’re done. On the Speccy? You could create an atan table and get the value from it using dy/dx as an index. But there are two issues here:

  1. Division dy/dx on the Z80 is slow. Our processor doesn’t have native division; it requires a subroutine, which takes tens or hundreds of clock cycles. We wanted speed, but got slowdowns.
  2. How to use dy/dx as a table index? The ratio dy/dx can range from zero to infinity! A 256-byte table won’t cover this entire range with sufficient precision. Even if you cheat and calculate the angle only within 0-45 degrees (where 0 <= dy/dx <= 1), you still need to scale this ratio somehow, which again requires division or complex multiplication.

So, a direct table-based atan(y/x) on the Z80 is neither the most elegant nor the fastest solution. We need to bypass this division problem!


Our Trick: Logarithms and Two Tables

And here’s our trick! Instead of calculating dy/dx (which involves division), let’s recall a property of logarithms: log2(Y / X) = log2(Y) - log2(X). Division has turned into subtraction! And the Z80 performs subtraction instantaneously. Bingo!

The idea is this:

  1. We have dx and dy. First, we ‘normalize’ them: make them positive (take the absolute value) and, if necessary, swap them so that one coordinate (let’s call it X) is always greater than or equal to the other (let’s call it Y). This is necessary to work only with angles from 0 to 45 degrees and to make the logarithm difference convenient. We keep track of which transformations we made (signs and whether a swap occurred).

  2. Now we have positive X and Y, where X >= Y. We need to find log2(X) and log2(Y). For this, we have our first table: log2tab. It stores log2(number) for each number from 0 to 255, but not just the raw value; it’s scaled (e.g., by 32) to maintain precision. We simply take log2tab[X] and log2tab[Y].

  3. Calculate the difference: log_difference = log2tab[X]log2tab[Y]. This is a fast subtraction!

  4. This "log difference" is related to the ratio X/Y. The smaller the difference, the closer X and Y are (angle closer to 45 degrees). The larger the difference, the smaller Y is compared to X (angle closer to 0). We need a table that, based on this log difference, tells us which angle between 0 and 45 degrees corresponds to it. This is our second table: atan2pow_tab. It stores angle values (scaled) indexed by the log difference.

In summary: Two small tables (256 bytes each), two lookups in the first table, one fast subtraction, one lookup in the second table – and we get a "base" angle in the 0-45 degree range! No slow division!


Putting It All Together: Algorithm Explained

Here’s how it looks entirely, step by step:

  1. Input: We get dx and dy.

  2. Special Cases: Check if dx=0 and dy=0 (zero vector, no angle) or if one of them is zero (angle 0, 90, 180, 270). Return the corresponding value quickly.

  3. Normalization: Take the absolute values x = abs(dx), y = abs(dy). Store the signs of dx and dy (e.g., in register bits). Compare x and y. If x < y, swap them (x = y_original, y = x_original) and remember that they were swapped (a ‘mirror’ flag). Now we are guaranteed to have x >= y > 0, meaning both are non-zero (except for the special cases we handled).

  4. Log Difference: Go to log2tab with index x, get logX_scaled. Go to log2tab with index y, get logY_scaled. Calculate k_scaled = logX_scaled - logY_scaled.

  5. Base Angle: Use k_scaled as an index for atan2pow_tab. Take the value ang_0_45_scaled (a number from 0-255, representing 0-45 degrees).

  6. Scaling: Scale ang_0_45_scaled to a 16-bit value (0-65535), which covers 360 degrees. Simply shift it left by 5 bits (<< 5). The result is a number representing the angle in 0-45 degrees, but now in 16-bit scale.

  7. Final Assembly: Use the saved flags (signs of dx, dy and mirror) to ‘reflect’ this 16-bit angle into the correct quadrant. If ‘mirror’ was set – subtract from 90 degrees (in 16-bit scale). If dx was negative – subtract from 180. If dy was negative – subtract from 360. This is done with simple 16-bit subtractions.

And here’s our result – a 16-bit number accurately representing the angle of the vector (dx, dy)!

Here’s how the table generators look in JS (just to illustrate what they contain) and a rough Z80 assembly implementation:

// JS code for table generation
const log2tab = new Uint8Array(256);
const atan2pow_tab = new Uint8Array(256);

function prepareTables() {
	// log2
	let i = 0;
	for (i = 0; i < 256; i ++) {
		log2tab[i] = (i > 0) ? Math.floor(Math.log2(i) * 32) : 0;
	}

	// atan(2^k)
	for (i = 0; i < 256; i ++) {
		atan2pow_tab[i] = (i > 0) ? Math.floor(Math.atan(Math.pow(2, - i / 32)) * 256 / (Math.PI / 4)) : 255;
	}
}

prepareTables(); // Generate tables
// Now log2tab and atan2pow_tab can be copied into assembly code as byte data

And here’s the Javascript code for calculating the angle using our method.

function logAngle(x, y) {
	// Get "clean" x, y values in the 0 to 45 degree range
	if (x === 0 && y === 0) {
		// Special case for zero vector
		return 0;
	}

	let is_x_neg = 0;
	if (x < 0) {
		is_x_neg = 1;
		x = 0 - x;
	}
	let is_y_neg = 0;
	if (y < 0) {
		is_y_neg = 1;
		y = 0 - y;
	}
	let is_mirr = 0;
	if (x < y) {
		// Exchange x and y
		is_mirr = 1;
		let t = x;
		x = y;
		y = t;
	}
	// Now X and Y are configured for an angle from 0 to 45 degrees
	let ang = 0;
	if (y == 0) {
		// Special case - sub-angle of 0 degrees
	} else {
		// Calculate the angle using the simplified formula atan(2^k), where k = log2(x) - log2(y)
		ang = atan2pow_tab[log2tab[x] - log2tab[y]];
	}

	// Place the obtained value into the correct quadrant
	// To preserve precision, shift the value left by 5 bits, thus expanding it to 16 bits

	let ang16 = (ang << 5);
	if (is_mirr) {
		ang16 = 0x4000 - ang16;
	}
	if (is_x_neg) {
		ang16 = 0x8000 - ang16;
	}
	if (is_y_neg) {
		ang16 = 0x10000 - ang16;
	}

	//ang16 = ang16 & 0xff00;  // If 8-bit precision is needed

	return ang16 * 360 / 65536;  // If a value in the 0 to 360 range is needed
}

And finally, fanfare! Our final Z80 Assembler code, which does the same as the Javascript code above, but tailored for our beloved Speccy.

; Calculate 11-bit angle value (aligned to 16 bit)
; Input: HL = (Y1, X1), DE = (Y2, X2) - Start and end points of the vector, [0, 0xff], unsigned values.
; Output: HL = angle value from 0 to 0xffff (0 = 0 degrees, 0xffff = 359.9 degrees), only the most significant 11 bits hold meaningful value
math_angle8x8
	ld c, 0		; Keep flags here. C.0 = is_x_neg, C.1 = is_y_neg, C.2 = is_mirror
	ld a, e
	sub l		; A = dx
	jr nc, .a1	; dx >= 0
	; dx < 0
	set 0, c	; is_x_neg = 1
	ld e, a
	xor a
	sub e		; dx = 0 - dx
.a1	ld e, a
	; check dy
	ld a, d
	sub h
	jr nc, .a2
	; dy < 0
	set 1, c
	ld d, a
	xor a
	sub d
.a2 ld d, a
	or e	; A = dx | dy
	jr z, .a3	; dx = 0, dy = 0, special condition
	ld a, e
	cp d
	jr nc, .a5	; dx >= dy
	; dx < dy, we need mirroring
	; A = E here
 	set 2, c
	ld e, d
	ld d, a
	; D and E were exchanged
.a5	ld a, d
	or a
	jr z, .a4
	; All flags were set, let's calculate
	ld l, e
	ld h, log2tab >> 8	; Upper byte of the table address
	ld a, (hl)			; A = log2(dx)
	ld l, d
	sub (hl)		; A = log2(dx) - log2(dy)
	ld l, a
	ld h, atan2pow_tab >> 8	; Upper byte of the table address
	ld a, (hl)		; A = atan2pow_tab(log2(dx) - log2(dy))
	; We got angle value from 0 to 45
	; Now mirror this value back
	ld l, a
	ld h, 0
	add hl, hl
	add hl, hl
	add hl, hl
	add hl, hl
	add hl, hl
	bit 2, c
	jr z, .a6
	; Mirror 0..45 to 45..90 if flag was set
	ld de, 0x4000	
	ex de, hl
	sbc hl, de
.a6	bit 0, c
	jr z, .a7
	; Mirror X
	ld de, 0x8000
	ex de, hl
	sbc hl, de
.a7 bit 1, c
	jr z, .a8
	; Mirror Y
	ld de, 0
	ex de, hl
	sbc hl, de
.a8	; Result in HL
	; Main return point
	ret
.a3 ; Special case: dx = 0, dy = 0
	ld hl, 0
	ret
.a4 ; Special case: dy = 0
	ld hl, 0
	jr .a6	; Let's use the correct quadrant

; These tables should be aligned by 256 bytes in RAM
	align 256
log2tab
	; These values are calculated as floor(log2(i) * 32) where i = 0..255
	db 0, 0, 32, 50, 64, 74, 82, 89, 96, 101, 106, 110, 114, 118, 121, 125
	db 128, 130, 133, 135, 138, 140, 142, 144, 146, 148, 150, 152, 153, 155, 157, 158
	db 160, 161, 162, 164, 165, 166, 167, 169, 170, 171, 172, 173, 174, 175, 176, 177
	db 178, 179, 180, 181, 182, 183, 184, 185, 185, 186, 187, 188, 189, 189, 190, 191
	db 192, 192, 193, 194, 194, 195, 196, 196, 197, 198, 198, 199, 199, 200, 201, 201
	db 202, 202, 203, 204, 204, 205, 205, 206, 206, 207, 207, 208, 208, 209, 209, 210
	db 210, 211, 211, 212, 212, 213, 213, 213, 214, 214, 215, 215, 216, 216, 217, 217
	db 217, 218, 218, 219, 219, 219, 220, 220, 221, 221, 221, 222, 222, 222, 223, 223
	db 224, 224, 224, 225, 225, 225, 226, 226, 226, 227, 227, 227, 228, 228, 228, 229
	db 229, 229, 230, 230, 230, 231, 231, 231, 231, 232, 232, 232, 233, 233, 233, 234
	db 234, 234, 234, 235, 235, 235, 236, 236, 236, 236, 237, 237, 237, 237, 238, 238
	db 238, 238, 238, 239, 239, 239, 239, 240, 240, 240, 241, 241, 241, 241, 241, 242
	db 242, 242, 242, 243, 243, 243, 243, 244, 244, 244, 244, 245, 245, 245, 245, 245
	db 246, 246, 246, 246, 247, 247, 247, 247, 247, 248, 248, 248, 248, 249, 249, 249
	db 249, 249, 250, 250, 250, 250, 250, 251, 251, 251, 251, 251, 252, 252, 252, 252
	db 253, 253, 253, 253, 253, 253, 254, 254, 254, 254, 254, 255, 255, 255, 255, 255

atan2pow_tab
	; These values are calculated as floor(atan(pow(2, - i / 32)) * 256 / (PI / 4)) where i = 0..255
	db 255, 252, 248, 245, 241, 238, 234, 231, 227, 224, 220, 217, 214, 210, 207, 203
	db 200, 197, 194, 190, 187, 184, 181, 177, 174, 171, 168, 165, 162, 159, 156, 153
	db 151, 148, 145, 142, 140, 137, 134, 132, 129, 127, 124, 122, 119, 117, 115, 113
	db 110, 108, 106, 104, 102, 100, 98, 96, 94, 92, 90, 88, 86, 84, 83, 81
	db 79, 78, 76, 75, 73, 71, 70, 68, 67, 66, 64, 63, 62, 60, 59, 58
	db 57, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41
	db 40, 39, 38, 38, 37, 36, 35, 34, 34, 33, 32, 32, 31, 30, 30, 29
	db 28, 28, 27, 26, 26, 25, 25, 24, 24, 23, 23, 22, 22, 21, 21, 20
	db 20, 19, 19, 19, 18, 18, 17, 17, 17, 16, 16, 16, 15, 15, 15, 14
	db 14, 14, 13, 13, 13, 12, 12, 12, 12, 11, 11, 11, 11, 10, 10, 10
	db 10, 9, 9, 9, 9, 9, 8, 8, 8, 8, 8, 8, 7, 7, 7, 7
	db 7, 7, 6, 6, 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 5, 5 
	db 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3
	db 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2
	db 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2
	db 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1


Why is this cool?

So, what did we achieve by using this trick with logarithms and tables?

  • Speed! No slow division subroutines! All the magic lies in memory accesses and fast integer operations. The angle is calculated very quickly, without ‘slowing down’ the game.

  • Size! Only 512 bytes for two tables, plus a bit of code. This is suitable for most games, even on 48K ZX models.

  • Precision! We get an 11-bit angle value, which provides very decent resolution (approximately 0.175 degrees per unit). For game purposes, this granularity is more than sufficient! If such precision isn’t required, and you want to fit within 8 bits (which would be about 1.4 degrees per unit), you can simplify the code further.

  • Elegance! Transforming division into subtraction using logarithms is an elegant mathematical solution, perfectly suited for limited hardware.

This algorithm is an excellent example of how seemingly complex problems can be solved on the Z80 using clever mathematical ideas and efficient memory utilization. It’s precisely such discoveries that allowed those unforgettable games to be created on the Speccy!


Conclusion

So, we’ve journeyed through this path – from crude quadrants and rough estimations to obtaining a sufficiently accurate angle value using logarithm tables and arctangents of powers of two. We’ve seen how a task that seems impossible without floating-point and ‘out-of-the-box’ trigonometric functions can be solved quickly and efficiently on our good old 3.5 MHz Z80.

This algorithm is just one of many examples of the ingenious tricks and elegant solutions developers used (and still use!) for retro platforms. Every clock cycle, every byte of memory mattered. And instead of despairing over limitations, people devised algorithms like this, transforming perceived drawbacks into opportunities for creative approaches.

It is in such problems and their solutions that the full beauty and art of retro hardware development are revealed. It’s not just coding; it’s optimization at the edge of what’s possible, seeking non-obvious paths, and employing mathematical tricks that might be long forgotten in a world of gigahertz and gigabytes.

I hope this article was not just a technical breakdown for you, but also a small journey into the world of those very solutions that made games on the ZX Spectrum so special. Perhaps you’ve seen how this algorithm can be applied in your own projects? Or maybe you remembered other clever ways to calculate angles or other functions on the Z80?

Feel free to share your thoughts, ask questions, or tell us about your own discoveries in the comments below this post! Sharing knowledge is the most valuable thing in our small but friendly retro community.

Thank you very much for your attention! See you next time in the world of retro programming!

With love for the Speccy and its magic,

P.S. All code can be downloaded from our Git repository.

The post Angle Calculation on Z80: Doing the Impossible at 3.5 MHz appeared first on ZX Online - Modern ZX Spectrum Games.

]]>
https://zxonline.net/angle-calculation-on-z80-doing-the-impossible-at-3-5-mhz/feed/ 2
One Classic to Rule Them All! https://zxonline.net/one-classic-to-rule-them-all/ https://zxonline.net/one-classic-to-rule-them-all/#respond Mon, 23 Dec 2024 10:29:15 +0000 https://zxonline.net/?p=154894 The post One Classic to Rule Them All! appeared first on ZX Online - Modern ZX Spectrum Games.

]]>

Dear friends and fans of the Speccy!

Firstly, we warmly congratulate all Spectrum enthusiasts on the upcoming holidays – Christmas, New Year, and another Christmas! We wish you and your families peace and well-being!

I am happy to announce that today we released the new game Lone Lunner, which my team and I at ZX Evolution have been preparing for almost a whole year.

This is a very dynamic and fun classic platformer, inspired by the game Lode Runner, in which the character cleverly climbs ladders to collect gold bars scattered throughout the labyrinth. At the same time, he must avoid encountering danger in the form of two, or sometimes three, guards who are trying to catch our hero and do something bad to him.

The hero is aided by his trusty eternal match, which allows him to burn brick blocks to his left or right. The hero can escape into these holes, as well as trap not very clever guards, who sometimes get out of captivity, and sometimes die. After all, the burnt blocks are restored very quickly.

This game is probably one of the most classic games of all time, and it has already been implemented in one form or another on every platform.
And on the classic Spectrum, you can find about a dozen similar games.

So why did we make another version?

In fact, the level of implementation of Lode Runner type games on the classic Spectrum is depressing. Those who have played it on other platforms do not want to play these semi-amateur monochrome crafts at all. Moreover, in implementing this project, we pursued another goal.

The successor to the Spectrum – the newly made ZX Spectrum Next – is quite popular among retro fans in Europe, but it is almost not common in Russia and other post-Soviet republics. At the same time, the TS Conf project is quite common in the Russian Federation (actually, this is the name of the firmware for the ZX Evolution computer). As it turned out, the platforms are very similar, although they have different hardware implementations.

Our big task was to make these two platforms work together. The result was a concept of architecture and a set of libraries that allows the same program code to be compiled for both platforms simultaneously, with almost no modification.

This concept will be further developed, but we are already using it in our next project (which I will talk about in the next article).

In our implementation of Lone Lunner, we used the best classic graphics from the NES version of the game, and we also created our own modern setting, which we hope you will also like.

The “artificial intelligence” of the enemies was also written from scratch, and therefore the game became more difficult and interesting. You can choose one of two difficulty modes and even remove the enemies completely (training mode). But to those who completely complete the game in NORMAL mode and do not go crazy at the same time, I suggest erecting a monument during their lifetime 🙂

In general, I urge you to download the game, donate and support the developers with kind words. This is very important in our difficult times.

The game is available both on ZX Online and on itch.io. We have to set a higher price per copy on itch.io because the money from there reaches us considerably diminished in its amount. So please don’t judge us too harshly. And even better – buy it on our website! The Robokassa service accepts plastic cards from any country. The Volet e-wallet, after a little bit of dancing around, also easily accepts funds from any European country.

Turtles Fighters

Soon (promised in January 2025), another completely new game based on the Teenage Mutant Ninja Turtles will be released on our resource. There is not much information about the game yet, but there are already screenshots. The game will be released for the TS Conf and Sprinter platforms.

And that’s all the news for today, thank you for your attention!

The post One Classic to Rule Them All! appeared first on ZX Online - Modern ZX Spectrum Games.

]]>
https://zxonline.net/one-classic-to-rule-them-all/feed/ 0
Adventures in a Magical Forest https://zxonline.net/adventures-in-a-magical-forest/ https://zxonline.net/adventures-in-a-magical-forest/#respond Sat, 04 May 2024 15:26:06 +0000 https://zxonline.net/?p=154236 The post Adventures in a Magical Forest appeared first on ZX Online - Modern ZX Spectrum Games.

]]>
Wow, friends! The long-awaited moment has arrived. You have waited and your wait has been rewarded! And there are exactly two news and both are good 🙂

First great news

Today a completely new game, The Land of Rustles 2, from the author with the nickname Slip, was released on our website. This is a continuation of the adventures of the Otter with a human face and Smoker in the amazing world in which the Rustles live. This time Otter will have to solve several difficult problems in a magical forest inhabited by different characters – both good and evil. The story begins with the fact that little Rustle falls ill and the cat Murka needs a magic book to treat her. As you progress through the game, you will have to solve many quests, and also learn how to dashingly jump and throw battle stars, because there are many enemies in the forest, and they do not sleep!

The game has gorgeous full-color graphics, because it is designed to run on a modern ZX Spectrum clone called TS-Conf. And if you don’t have a “hardware” computer of this architecture, it doesn’t matter, the downloaded archive contains a version of the game for Windows (this is a newest UnrealSpeccy emulator with autorun of binary files of the original game configured).

Of particular note is that the game has full-screen scrolling (and this is rare even for modern ZX Spectrum clones) and the ability to listen to music and sounds under NeoGS, they are great! By the way, the music for the game was written by the famous musician MmcM, whose tracks you have definitely already heard in other games. In general, 9 people participated in the development, even I found a job – I translated the game into Russian and participated in testing 🙂

P.S. By the way, Slip has a community on Boosty: https://boosty.to/zxevolution where we discuss games being developed, and also plan future projects. You can do this too 🙂

In general, the game is a fire, absolutely recommended!

Second great news

The second good news is that we have finally launched payment for games through itch.io, where seamless payments with Paypal and plastic cards are available! We’ve linked our itch.io and ZXOnline accounts so that purchases you make there will count towards ZXOnline, giving you the same benefits as if you bought directly from ZXOnline (such as the ability to write reviews and rate games, and many more to be implemented in the nearest future).

Currently, purchase through itch.io is available for three games – New Rustles 2, EL BIGOTUDO and Zedex Tanks, but in the very near future we will add all the rest. All new games will also be automatically posted on itch.io and ZX Online.

How to buy a game through itch.io? It’s very simple. On the game page, under the payment widget, you will see the itch.io branded icon and detailed instructions on what to do. You can even just search the game on itch.io to buy directly, but in this case you will not get a license for the game on ZXO and all the features that come with it.

That’s it, there’s no point in wasting any more time, it’s time to start exploring the Magical Forest! All the best and thank you!

The post Adventures in a Magical Forest appeared first on ZX Online - Modern ZX Spectrum Games.

]]>
https://zxonline.net/adventures-in-a-magical-forest/feed/ 0
When the Moon Isn’t What It Seems! https://zxonline.net/when-the-moon-isn-t-what-it-seems/ https://zxonline.net/when-the-moon-isn-t-what-it-seems/#respond Sat, 06 Apr 2024 12:37:21 +0000 https://zxonline.net/?p=154017 The post When the Moon Isn’t What It Seems! appeared first on ZX Online - Modern ZX Spectrum Games.

]]>
Greetings, friends!

How often do you look closely at the Moon while walking around the city at night or in the evening? Well, it’s the ball hangs and glows, nothing special. But this very superficial point of view can be very misleading. Why suddenly and who decided that the Moon has always been like this? Or maybe someone replaced it and no one noticed?

After a long time, we finally have a new game released for the “classic” ZX Spectrum. This is a puzzle-platformer with elements of a quest, which tells the story of the adventures of a retired intelligence agent nicknamed “EL BIGOTUDO” (real name Carlos Fernando Garcia), who, as usual, went into a bar, had a drink, but then very, very strange things began to happen.

The authors of the game do not reveal details of the plot, the number of game screens and the goals of the game. One thing we can say for sure is that the graphics are made at a high level, somewhat reminiscent of the graphics from “White Jaguar”, but more intense, since the actions take place inside some kind of space station or spaceship. The animation of the main character and hostile mobs is drawn in great detail. The music from the famous composer MmcM is also excellent.

The game offers us a large number of riddles and game situations; there is also that same “spirit of exploration” that captivated us in the distant 80-90s, when we discovered the riddles of the game and found various features without hints from a book or the Internet. In general, there is definitely something to explore here.

Hurry up to download the game now and don’t forget to give at least a small bonus to the authors, because to make a game of this level you need to spend a huge amount of time and effort. In addition, you can rate the game and leave your review.

If you are in the Russian Federation, then you can pay using Robokassa – it accepts euros and automatically converts into Russian rubles. If you are not in the Russian Federation, you can pay with USDT or bitcoins using the Cryptomus payment gateway, and also use a regular plastic card through AdvCash or the same Robokassa – they accept regular Mastercard and Visa (which are not in the Russian Federation). We have a 2 EUR discount on payments via AdvCash, because when making your first payment, AdvCash may ask you for confirmation of documents. This is inconvenient, which is why we provide a discount.

If you have questions about the game or payment, you can ask them right below this text.

Thanks for your attention and have a good play!

The post When the Moon Isn’t What It Seems! appeared first on ZX Online - Modern ZX Spectrum Games.

]]>
https://zxonline.net/when-the-moon-isn-t-what-it-seems/feed/ 0
Welcome to World Without Borders! https://zxonline.net/welcome-to-world-without-borders/ https://zxonline.net/welcome-to-world-without-borders/#comments Sun, 24 Dec 2023 10:27:11 +0000 https://zxonline.net/?p=153613        

The post Welcome to World Without Borders! appeared first on ZX Online - Modern ZX Spectrum Games.

]]>
Hi!

First of all, Christmas and New Year are coming, so I would like to congratulate you from the bottom of my heart and from our whole team and wish you warmth, joy and unforgettable moments both on these days and during the whole 2024!

Thank you, friends, that despite everything you stay with us, visit the site, take interest in new products, help developers in their hard work – leave reviews and ratings, make reposts of games, shoot streams, and someone even throws a coin :). Even though it’s quite difficult nowadays. Once again, thank you very much.

I have three great news for today, which probably can hardly be called a Christmas present, but it’s good news nonetheless.

If you don’t have time to read the big text, you can scroll all the way down to the heading “Let’s summarise!“.

News #1. The country where Rustles live

Rustles 2 gameplay 4
Rustles 2 gameplay 4

The promising project “The Land of Rustles 2” is being aggressively developed and is 90% ready now. All worlds and graphics are drawn, quests are being finalised and the engine is being actively tested. The main coder of the project – Slip – every week pleases us with fresh gameplay videos on his channel in Boosty. Initially the game was planned to be released by Christmas, but something went wrong, and the deadline was postponed to January next year. But despite the fact that the game itself is not yet available, you can get a little ahead of the game and buy the licence at a reduced price on the game’s page now and then wait for the release in peace. Besides some financial benefit you will support the developers in their endeavour to finish the game on time, and maybe they will finally be inspired by the idea of adapting it for ZX Spectrum Next 🙂

News #2. Again about payment on the site and the world without borders

We’re expanding payment options on the site, and although we haven’t got Paypal (and probably won’t in the foreseeable future), there are more and more serious competitors that can provide money acceptance worldwide (not just where some guys allowed it). Yes, I’m talking about cryptocurrencies, of course.

Cryptomus crypto payment acceptance system has been connected for quite a long time and works fine, but there is a certain “entry threshold” for users to start using crypto en masse. Apparently because cryptocurrencies in general have a reputation for being unreliable (rates float, no guarantees and so on). That was all true in the past, but not now.

Now there is a very good thing – it’s called USDT. This cryptocurrency does not “float”, the value of 1 USDT is always exactly equal to 1 USD. It is convenient, there is no risk. At the same time, this currency is not managed by any government and therefore it cannot be blocked – you can pay and accept payments in the USA, Europe, as well as in Russia and China.

Besides, it is easier than ever to create an e-wallet and start paying with USDT – you can register an account on any of the cryptocurrency exchanges (for example, Binance), but there you will be asked to confirm your identity. Or you can start a wallet directly in Telegram and there you won’t need any documents. If you already use Telegram, it’s just a few clicks for you! (And if you don’t use it – install it immediately, it’s the coolest messenger today).

Here you can find very simple instructions on how to start a USDT wallet in Telegram, top it up and immediately start buying anything. Yes, yes, you can buy games on ZXOnline without any problems.

And if you don’t like the idea of paying via USDT, you can try to pay via the new international payment system ROBOKASSA, which we added recently. They have almost 100% passability for Mastercard cards, and Visa cards also work (though not in all countries).

News #3. An offer for game developers that they won’t be able to refuse 🙂

If you ask any indie developer now – where do you host or plan to host your game? The answer would most likely be, “on itch.io“. Of course, it’s a very convenient popular site, which for all its convenience is not without some drawbacks.

On ZXOnline we are trying to make more convenient service, “sharpen” it for ZX Spectrum users, make the site and games really multilingual, add payment systems, so that you can pay from anywhere in the world, introduced a rating system, protected from “cheating”, increase awareness of users about new games – for example, when adding a new game, all users of the site (and there are already more than 5000 of them) will receive an information letter.

That’s why we have a suggestion for authors: if you’ve already published a game on itch.io or elsewhere, why not post it on ZXOnline as well? After all, you don’t really have to do anything.

Just visit this page, submit a link to your game on itch.io or any other resource, and we’ll do the rest!

And what exactly will we do?

  • We will create a clone of your game’s page on ZXOnline and carefully transfer all information – description, pictures and video,
  • We will translate the information into Russian (and later on into other languages),
  • We will inform all ZXOnline users about your game and invite them to play and leave a review,
  • We will keep track of updates to your game and notify your customers,
  • We will regularly or on demand transfer your earnings to your Paypal or other means.

What will you get?

  • The ability to sell games in more countries (thanks to the availability of crypto-payments and translation of game descriptions into other languages),
  • Ability to receive advanced reviews with anti-cheat protection,
  • Ability to place a free demo version of the game (if the full version is paid) and try the game directly in the browser,
  • Opportunity to “force” sales to 5000+ potential buyers – real ZX Spectrum users,
  • The opportunity to participate in the ZX Online Awards 2024, which we will hold next spring,
  • A loyal and energised audience of ZX Spectrum fans,
  • Some other ZX Spectrum-specific things that will probably never appear on itch.io (we’ll keep them secret for now).

So, it turned out to be a lot of text and information, so don’t forget anything,

Let’s summarize!

  1. The game “The Land of Rustles 2” is coming out soon, you can buy early access now at a discounted price.
  2. You can dive into the world of cryptocurrencies without risk or sending documents, just by starting a cryptocurrency wallet on Telegram and start paying for games on ZXOnline or goods in ANY country in the world! Or try the new Mastercard payment system ROBOKASSA.
  3. If you are a ZX Spectrum game developer – leave a link to your game on this page and we will make sure that more people will know about it (see above).

And of course, thanks again for your attention, for staying with us all this time and all the best for the new year!

MERRY CHRISTMAS AND HAPPY NEW YEAR, FRIENDS!

 

 

 

 

 

The post Welcome to World Without Borders! appeared first on ZX Online - Modern ZX Spectrum Games.

]]>
https://zxonline.net/welcome-to-world-without-borders/feed/ 1
May the Games Live Forever https://zxonline.net/may-the-games-live-forever/ https://zxonline.net/may-the-games-live-forever/#comments Fri, 14 Jul 2023 22:36:35 +0000 https://zxonline.net/?p=152953 The post May the Games Live Forever appeared first on ZX Online - Modern ZX Spectrum Games.

]]>
Hi!

How many Spectrum games do you know? 50? Maybe even 100? Meanwhile, there are about 20 thousand ZX Spectrum games. (You can download this image, made up of all the games’ loading screens, and see for yourself how MANY there are). Yes, I admit, more than half of them are simple crafts a bit more complicated than “Hello, world!”, on which you don’t even want to waste your time, than play them seriously. But among the rest there are quite worthy of consideration.

If you think about it, there’s no difference between a brand new game that came out yesterday and one that came out in 1985, provided you haven’t played either one yet. I’d even say that the 1985 game has a bit of a head start, since it already has both ratings and fans.

Anyway, closer to the topic

I’ve already mentioned before that we have a website Viva Games, where all (or almost all) retro games for ZX Spectrum are collected. No so long ago, we have done a colossal work to fix and restore almost 5000 (!) original game descriptions in English, Spanish, Italian and other languages, as well as to translate them into English and Russian. This titanic work took more than a year and now we are starting to gradually publish these descriptions on the website.

And to make it more interesting for you, we have hand-picked a few hundred underrated games – little-known, but at the same time original and in some ways distinctive.

These games will be regularly published in the official Viva Games group in Telegram. We have provided each of them with a small description, our summary and an opportunity to play online directly on Viva Games site.

What is the purpose of all this movement?

  • For “quite good” games – it’s a chance not to get lost in history and to get a second life, because we believe that games remain alive as long as they are played.
  • For Spectrum fans – it’s a chance to discover something new, and sometimes to rethink old games that you played as a kid, but without a description you either didn’t understand how to play or didn’t understand what the point is.
  • For “active dreamers”, it’s a source of new ideas. Maybe you want to make a version of your favorite game for ZX Spectrum Next or write a sequel to a classic?

Besides the promised game descriptions, we are going to publish game analytics, voting, contests and other things, because few people love ZX Spectrum games as much as we do. So it going to be interesting. Join us!

By the way, this channel exists in two versions – if Russian is closer to you – here is Russian version of the channel. Oh, the Viva Games website itself is not a bit ready yet for English, but we going to fix this in a week or two.

P.S. The vast majority of descriptions are taken from open sources – from the World of Spectrum website, as well as many others. Descriptions in text format, which is inconvenient for modern web, had to be adapted manually. Thanks to each and every person who painstakingly typed these texts from the covers of physical releases. We have kept all copyright and links in the texts, thank you!

See ya in the telegram channel!

With love to Speccy and Speccy amateurs,
Alexey Khaydukov (aka Epsilon) and Evgeny Kolesnikov (aka Buddy/ERA).

The post May the Games Live Forever appeared first on ZX Online - Modern ZX Spectrum Games.

]]>
https://zxonline.net/may-the-games-live-forever/feed/ 4