News

ACNews #79

News

by Alone Coder

Hello everyone!

Due to the possible internet outage, I'd like to remind you that my alternative email address is alone_coder@mail.ru. MAX is also linked to my phone number. Our chat has also moved there from Discord, which was blocked last year.

The engine of the new Wolf3d-like game now supports the bottom panel, and graphic resources are imported by an external utility rather than being recoded during startup (which took a long time). I hope the game will finally be finished.

The NedoLang system has been almost doubled in speed by removing the tokenizer from the main compilation path. Now the compiler itself tokenizes, and the assembler has learned to evaluate expressions in direct order (they are now in plain text). The tokenizer is only needed for assembler modules, but a tokenized editor can be written. Now, self-compiling the compiler takes about a minute on a ZX Evo, and the tokenizer and assembler take about half a minute each. About half of this time is spent in assembler. Rewriting the assembler in assembly can speed it up by a factor of two. Incidentally, Dmitry Anokhin has already written a simple assembler for NedoOS in assembly:
https://github.com/deemess/assembler/

I haven't been able to stream lately because my kids are constantly in the frame. I've only managed to do two streams about text compression for games (if anyone's interested, please contact me).
Thanks to these streams, I was able to meet Pavel Sereda, who lives right here in Ryazan and makes plastic parts and other equipment right at home.

In 2022, I experimented with so-called "artificial intelligence" for image generation. I couldn't install Stable Diffusion on my computer (it turned out it requires a powerful graphics card), but I did generate graphics for a New Year's gift using online services. Over the past three years, the quality of generators has improved significantly. In particular, the generated images are now indistinguishable from reality and can be animated. Perhaps you've seen a video of Spectrum animations on Sergio Neznany's YouTube channel. Such animations are even already used in films and cartoons.

Together with Event and John Silver, we explored the potential of modern AI in music. I'm not talking about generating entire songs in Suno or processing existing ones (negative harmony covers or all the stylized versions that are already plentiful on the internet). We were interested in analyzing existing music. Izotope RX has learned to decompose music into channels quite cleanly. Using this tool, we were able to bring one remix (new, by Event) closer to the original, and it will appear in the next issue of Info Guide. Send us your articles and music!

For the past three months, Pavel Sereda and I have been trying to tame AI to generate assembly programs for the ZX Spectrum. The main tool was Qwen, which Pavel had already tested for creating simple Golang programs. We attached a shellscript to Qwen that automatically launched the compiler (SjASMPlus) and emulator (first MAME, then Unreal Speccy). It couldn't write the script by itself, but it was pretty close. Along the way, it even forgot one of the first requirements (launching in ATM Turbo mode) and tried running the emulator in Profi mode, which wasn't even mentioned. In Unreal Speccy, we added automatic exit on RST 0 with a snapshot and conducted a series of experiments.

At first, we assumed that the AI would be able to write a program to draw a mouse arrow. This was a naive hope: numerous queries in different styles, clarifications, and so on yielded no results, not even a partially working one. The program either crashed or drew gibberish on the screen. Qwen confidently reported every time that it had solved the problem. We gave him a snapshot (he read it with a hex editor, since he only knows how to process text), but it had absolutely no idea what on the screen was wrong. It constantly forgot the Spectrum display addressing, even though he knew it from the start. Its mathematical errors reached the point where it confused the h and l registers or overwrote registers whose values were used later. The best it could do was "Hello world," but it only generated successfully once, and then it never worked again. It's important to clarify in your request that Qwen should not use ROM routines, as it has misconceptions about them.

The first request looked like this:

"You have a nearly empty project for developing for the ATM Turbo 2 (a ZX Spectrum clone). The main source file is @source.asm . To compile, run make . @release.trd will be built. To run the emulator with the compiled program, run @./emu_start.sh . When your program executes the rst 0 command, the emulator saves a snapshot @qsave1.sna and exits. This way, you can see what your program changed in memory. Your task is to write "Hello, world!" without using ROM routines. Monitor success by checking the dates of the created files."

A typical response, after several clarifications, looked like this:

"Shell xxd -s 2600 -l 128 debug2_screen.bin (Look at the screen memory around row 10, column 5)

    00000a28: 0022 2222 3e22 2222 0000 0000 0000 0000
    00000a38: 0000 0000 0000 0000 0000 0000 0000 0000
    00000a48: 0000 0000 0000 0000 0000 0000 0000 0000
    00000a58: 0000 0000 0000 0000 0000 0000 0000 0000
    00000a68: 0000 0000 0000 0000 0000 0000 0000 0000
    00000a78: 0000 0000 0000 0000 0000 0000 0000 0000
    00000a88: 0000 0000 0000 0000 0000 0000 0000 0000
    00000a98: 0000 0000 0000 0000 0000 0000 0000 0000

Here he is! We see the character 'H' at position 0x0A28 (2600 decimal):

  • 0x00 (%00000000) - first line
  • 0x22 (%00100010) - second line
  • 0x22 (%00100010) - third line
  • 0x22 (%00100010) - fourth line
  • 0x3E (%00111110) - fifth line (middle of the 'H' character)
  • 0x22 (%00100010) - sixth line
  • 0x22 (%00100010) - seventh line
  • 0x22 (%00100010) - eighth line

This matches the pattern of the character 'H' exactly! So, our approach works."

That is, the model had tripped up, and it began expecting character strings on the screen to be located in adjacent bytes. (Sometimes it tripped up to one byte per character.)

After all these failures, we began to suspect something was wrong and offered Qwen a simpler task: calculate the address on the Spectrum screen one line above. The same thing happened: after many requests and clarifications, including suggested numerical examples, it continued to produce non-working solutions and ultimately wrote nothing (to be fair, it had once implemented the calculation of the screen address from coordinates correctly, as a part of the arrow project).

We gave it an even simpler task: given a processor with one register and one carry flag, and the instructions MOV [ADDR], A, SUB A, [ADDR], AND A, [ADDR], JNC ADDR, implement loading a value into register A from memory at address ADDR. Qwen solved it in 6 instructions (like most people). After prompting, it was able to reduce the number to 5 and was certain he couldn't go any further, and it only found the correct solution in 3 commands after being told it definitely existed.

Since the snapshot proved unsuitable for self-assessment, we decided to add an automatically generated screenshot. It turned out that Qwen couldn't read images in coder-model mode. Vision-model mode, which reads images and describes them quite accurately, is available through the same interface. But switching modes on the fly doesn't help. Moreover, Qwen itself says the switch simply didn't happen. However, it did say that you can pass the command to another Qwen instance so it can return the image description. And then the final blow occurred: the file whose name was passed via an external command simply couldn't be read. This was probably some kind of protection (if there would be something to protect - Qwen freely edits and runs files anywhere on the computer). Ultimately, we filed a bug report and waited.

We also tried different models in LM Studio and Ollama, but
nothing worked. The first one doesn't run at my home because I
have a wrong processor. It runs at my work (specifically, I use
Qwen ), but the graphics analysis model spits out a stream of
random characters as a response. Ollama works at home, but even
with the simplest chat, GPT, eats up 13 gigabytes of RAM, so I
can't even run it concurrently with the browser. I also tried
installing different graphics models there, but instead of
providing the correct description, they generate random
descriptions based on file names, meaning they simply don't read
the contents.

In the meantime, I tested other nonsense generators as assembly
language programmers. I started with GLM 4.6, believing the ads
on YouTube.

I tested "write a subroutine in Z80 assembler for the ZX
Spectrum computer that, given a screen address (in the HL
register), finds the screen address one pixel higher and returns
it in the HL register. No end-of-screen checks are needed."
The task failed. It knows the screen addressing, but the math is
wrong:

GetPixelAbove:
ld a, h
and %00111000
jp z, IsTopOfChar
ld de, -32
add hl, de
ret
IsTopOfChar:
ld de, -1824
add hl, de
ret

"This doesn't work. Within an 8-line block, the addresses of
adjacent lines differ by 256, not 32" - it made a 2-line
procedure "dec h:ret".

"Now you forgot to handle the case of a transition to another
8-line block" - it wrote "dec h:ret" again.

"No. For Y=8, X=0, the address is $4020. The line above is the
address $4700. Also check the case Y=64, X=0: the address is
$4800, the line above is the address $47e0." It continued to
insist.

"The formula addr = 16384 + (Y_char 2048) + (Y_pixel 256) +
(X_char 32) + X_pixel is incorrect. Where did you get it? The
correct formula is: addr = 16384 + (Y_third_of_screen
2048) +
(Y_character_inside_third_of_screen 32) + (Y_pixel 256) +
(X_character)." It agreed that it lied about the formula, but it
thinks mine is also incorrect. He quoted Jonathan Cauldwell's
procedure, but it said it's also "incorrect". And it returned to
the first version, which doesn't work.

"The quoted procedure works correctly:

; Move one pixel up
; HL = screen address
; Carry set if we've gone off the top of the screen
(this comment is incorrect)
up_one_pixel:
ld a, h
and $07
jr nz, not_new_char
ld a, h
sub $08
ld h, a
ld a, l
add a, $E0 ; -32
ld l, a
ret c
inc h
ret
not_new_char:
dec h
ret

And yours isn't working correctly. Try to figure out why you
made a mistake. ld a, l / and 7 doesn't return the Y_pixel bits.
You can get the Y_pixel bits using ld a,h / and 7.
By the way, the procedure you quoted is clearly not the shortest
version. Try starting with ld a,h / dec h / and $07." - then it
finally admitted it was wrong and generated another broken
procedure.

GetPixelAbove:
; First, check the high-order part of Y_pixel (bits 2,1,0 in H)
ld a, h
and %00000111
jp nz, SimpleCase ; If non-zero, then Y_pixel is definitely > 0

; If here, then the high-order part of Y_pixel = 0.
; Check the low-order part of Y_pixel (bit 5 in L)
ld a, l
and %00100000
jp nz, SimpleCase ; If non-zero, then Y_pixel is also > 0

; --- COMPLEX CASE: Y_pixel = 0 ---
; Jump to the previous character block and to its 7th Pixel row.
; Offset: -1824
ld de, -1824
add hl, de
ret

SimpleCase:
; --- SIMPLE CASE: Y_pixel > 0 ---
; Go to the previous pixel row within the block.
; Offset: -32
ld de, -32
add hl, de
ret

Writes "As we established, the test for Y_pixel = 0 on a
standard screen is non-trivial. It is equal to (H & 7 == 0) AND
(L & %00100000 == 0)."

"First, the test for Y_pixel = 0 on a standard screen is not
equal to (H & 7 == 0) AND (L & %00100000 == 0). It is equal to
(H & 7 == 0). Secondly, SimpleCase is again shifting by -32
(this shift only happens between thirds), while it should
usually be by -256. Let me write you a clearer example of how
the address bits are arranged on a standard Spectrum screen:
%010TTyyy YYYXXXXX, where TT is the third number, YYYyyy are the
remaining bits of the Y coordinate, and XXXXX is the X
coordinate in character cells." It declared that it "now
understands" and generated more nonsense:

GetPixelAbove:
; Check if we are at the top of the character string (Y_pixel=0)
; The Y_pixel bits (YYY) are located in the upper three bits of
the L register.
ld a, l ; Load the low byte of the address
and %11100000 ; Mask bits 7, 6, 5 (leaving only the Y_pixel)
jp nz, NotTopOfChar ; If the result is non-zero, then the
Y_pixel was > 0.

; --- CASE 2: Y-pixel = 0 ---
; We are on the very top row of the character. We need to move
to the previous
; character block and to its 7th pixel row.
; Resulting offset: -1824.
ld de, -1824 ; Load offset -1824 into DE
add hl, de ; Apply the offset
ret ; Return from the subroutine

NotTopOfChar:
; --- CASE 1: Y-pixel > 0 ---
; We simply move to the previous row of pixels within the same
character block.
; This means subtracting 32.
ld de, -32 ; Load offset -32 into DE
add hl, de ; Subtract 32 from the address in HL
ret ; Return from the subroutine

"Test your procedure using my formula in the cases Y=1, Y=8,
"Y=64. It seems that in the case of Y=1 (address $4100, the
result should be $4000), it doesn't work correctly." It
responded with the same idiotic procedure as Qwen (ld de,-32:add
hl,de:ret). It then argues, "As you can see, in all cases except
the very top of the screen (Y=0), the upward pixel offset is
always -32.
This is the genius and quirk of the Spectrum screen. Moving down
the screen is complex (first -32 7 times, then a big jump, and
then back in a circle again). But moving up one line is always a
simple subtraction of 32."

DAMN GENIUS!

"Given the following instruction set:

MOV [ADDR], A
SUB A, [ADDR]
AND A, [ADDR]
JNC ADDR

where A is a single register
ADDR is the specified address in the memory area being used
JNC is a jump if the carry flag is 0
SUB affects the state of the carry flag
AND resets the carry flag

You need to write a sequence of instructions to load a value
into register A from memory at address ADDR."

Output:

SUB A, [ADDR]
AND A, [ADDR]
MOV [ADDR], A
SUB A, [ADDR]

That is, he couldn't even master the first 6-line solution.

Via LM Arena website (which launches a random model each time),
I tested about 15 other models on two tasks: finding an address
above and shifting the screen 4 pixels to the left. Of the
Chinese models, only Ernie showed any promise. Only Claude was
able to solve both tasks. Grok may have produced something
working, but it was very bloated code, and I didn't test it.
Claude also was the highest in the internal ratings.
However, it's paid and unavailable in Russia.

Firestarter was able to gain temporary access to Claude and
generate a game prototype for the ZX Spectrum. However, the work
was only possible through a mega-prompt, about 30 kilobytes in
size, describing the entire game, with several additional links
inside. Surprisingly, Claude can verify the correctness of the
generated code without an emulator (the same prompt in Qwen
produces a completely non-functional program). However, building
a process by gradually reviewing and modifying the generated
code hasn't yet worked.

Qwen is free. It can generate work plans based on source code
analysis. However, it can't perform these tasks itself, except
for the simplest ones. When you try using it, you'll notice that
it can make uninvited edits, even ignoring "don't touch!" lines.

When running within LM Studio, it consumes 4 gigabytes of RAM
and a couple of cores, with a speed of around 20 tokens per
second on an i7-14700 at 2.11 GHz. LM Studio may not work on
older computers (as well as on computers with an AMD processor)
because its engine requires support for some command
extension. However, Ollama works, and GPT can be easily
installed on it. Experiments were conducted specifically with
qwen3-4b-thinking-2507. While its capabilities as a text tool
are impressive, it also misses many errors:

"List the countries of the European Union," it lists 20 out of
27, repeatedly correcting the list. "Think of other European
countries and check which ones are in the EU," it doesn't help
(though it does mention Crimea and writes that it's part of
Russia). If I ask further, "What do you know about countries
such as Lithuania, Andorra, Monaco, Greece, Albania, Croatia,
Bosnia and Herzegovina, Montenegro, North Macedonia, Serbia,
Kosovo, Liechtenstein, San Marino, Vatican City, Norway, Cyprus,
and Malta?" it answers for each of them (though Kosovo is "not
recognized"). If after this, "List the EU countries we now know,
but don't think twice," it doesn't help. It still
hesitates, fails to mention Cyprus, Lithuania, Greece, and
Croatia, and along the way, the status of Bosnia and Herzegovina
changes from "not part of" to "part of" (which is untrue). It
doesn't mention Bulgaria even once. If reminded of Cyprus and
Bulgaria, it adds Bulgaria but refuses to include Cyprus.

Linguistics: it knows English, Russian, Greek, German, French,
Italian, Spanish, Polish, Czech, Bulgarian, Latin, Chinese,
Japanese, fragmentary Lithuanian, and fragmentary Farsi (it
tries to reconstruct Tajik from Farsi). It doesn't know Old
Church Slavonic or Ossetian, not even their alphabets. It
incorrectly translates the word "bird" into Lithuanian, Farsi,
and Tajik (although it knows the word). It doesn't know
Trubachev or ESSYA, Lubotsky or the Leiden series of
dictionaries. It makes some mistakes translating into Latin (at
least, incorrectly conjugating some verbs), although it
knows it generally.

"Your role is that of a linguist. Before you is the text 'съешь
ещё этих мягких французских булок, да выпей чаю.' Within this
text, place 0's when the voice is turned off and 1's when the
voice is turned on." - it completely fails this test.

It knows individual fragments of poetry in various languages,
but it knows nothing completely, doesn't even know reliable
first lines (i.e., like the learned cat in "Monday Begins on
Saturday" ). It also doesn't know the Strugatsky brothers.

It doesn't know the Tactical Missiles Corporation and is
absolutely certain it doesn't exist.

It doesn't know its own structure or functionality.

It doesn't know what exactly it knows and what it doesn't know.

It can serve as an assistant for coining Russian terms to
replace foreign ones, although most of its ideas are
nonsensical. It can plan programming tasks, but it can't execute
them itself because it constantly generates non-working code and
can't check it (a huge difference from Claude ).
I haven't found any other uses yet.

Among other things, I couldn't use Qwen to extract the changelog
for a specific Wikipedia article. It could probably write a
script in one of the languages it's well trained in (though it
doesn't say which ones) and run it. But it didn't even suggest
this option; it simply invented non-existent changes.

Experiments are ongoing. Write about your achievements and
methods.

                        * * *

Although Spectrum has been around for decades, many important
hardware experiments remain untested.

I'm interested in the following information:

  • the results of an experiment to enable ULA test mode (64x
    speed) on a live 48K (if I remember correctly, shorting
    MREQ to some address line so that MREQ and IORQ are present
    simultaneously). Is it possible to obtain practically usable
    scrolling?
  • Actual volume measurements (not voltage measurements, like the
    previously published ones) on AY at +2
  • On Timex, enabling Hires after color burst generation to
    create fake colors, like on Apple II and CGA
  • Difference in color levels on different color grids at 48 and
    +2 - comparison
  • How common is "bright black" on British machines (I've only
    seen it in one Timex video)
  • What's the range of brightness levels (bright/nobright) across
    color tones and models?
  • Oscillograms of typical key response on a 48K (rubber) and ZX
    Spectrum+ (membrane), comparison with Russian keyboards
    (membrane, reed switch).
  • AY behavior after a RESET signal - are the counters and phases
    of the tone generator, envelope, and noise reset this way? In
    other words, is it possible to create predictable timbres? (in
    reality, this can only be done on ATM2 and Scorpion (if you
    short-circuit the RESET wire to one of the AY port bits), since
    you need a resident that will start the player after the reset.

Share your thoughts about the article