GO WEST, часть 1

Я хочу попробовать собрать в максимально компактной форме более-менее всё, что нужно для того, чтобы либо портировать вашу программу на один из классических спектрумов, либо написать её с нуля сразу совместимой с классикой. Когда я говорю «классика», я имею в виду любую из следующих машин: ZX Spectrum 48K, 128K, +2, +2A, +2B или +3. На практике, многие из этих моделей очень похожи друг на друга с т.зр. программиста и реально важно отличать следующие три вида классических спектрумов: 48К или 128К/+2(«серый») или +2A/+2B/+3.

В целом, отечественные клоны обладают довольно высокой степенью совместимости с оригиналами и переделка вашей программы для Leningrad 48К на 48K классику или переделка вашей программы для Pentagon 128K на любую 128К классику скорее всего окажется возможной, зачастую даже необременительной. Тем не менее, различия есть, и если не принять их во внимание, можно очень легко получить спектрумовский софт, который ни на одном настоящем спектруме не заработает.

ПАМЯТЬ И ПОРТЫ


Про память нужно знать всего пару вещей. Во-первых, в общих чертах, расклад памяти вполне привычен. 48К — он и в Африке 48К. 128К и работа с младшими 5-ю битами порта #7FFD — всё как обычно, ничего нового (хотя у +2A, +2B и +3 есть две дополнительные страницы ПЗУ). У +2A, +2B и +3 есть также дополнительный порт #1FFD, который позволяет менять расклад спектрумовской памяти весьма кардинальным образом. Вот расклад этого порта по битам:

бит 0 режим адресации (0 — привычный, 1 — ненормальный) 
биты 1,2  формат памяти в ненормальных режимах, или…
бит 2 … старший бит выбора ПЗУ в привычном режиме
бит 3 мотор дискового привода
бит 4 строб порта принтера
Таблица 1: раскладка порта #1FFD.

Что реально делают биты 3 и 4 я не знаю и знать не хочу. Куда более интересны тут ненормальные режимы адресации. Выглядят они так (каждая строка начинается с состояния битов 2 и 1 порта #1FFD):

Бит 2 Бит 1 Страницы памяти (снизу вверх, т.е. страница с адреса #0000, страница с адреса #4000 и т.д.) 
00память составлена из страниц 0,1,2,3
01память составлена из страниц 4,5,6,7
10память составлена из страниц 4,5,6,3
11память составлена из страниц 4,7,6,3
Таблица 2: ненормальные режимы адресации для +2A/+2B/+3.

Экраны, как всегда, прибиты к страницам 5 и 7, таким образом, в конфигурации 00 у вас нет ни одного экрана, а в конфигурации 01 — их сразу 2. Т.к. чуть более старые спектрумы (128К и +2) не поддерживают порт #1FFD, на практике, очень мало какой-то софт использует эти дополнительные возможности по организации памяти. Но, если вы написали ваш софт на Скорпионе или Кае или одном из их родственников, есть ненулевая возможность что ваш софт полезет в порт #1FFD и попадёт в беду. Старые диспетчеры расширенной памяти из спектрум-прессы это не учитывают — будьте бдительны!

Вторая проблема при работе с памятью на классике заключается в том, что половина вашей памяти — «медленная»! Насколько медленная? смотря что с ней делать. Я хочу подробно описать методику расчёта тормозов медленной памяти во третьей части моей статьи (это отдельная большая и нетривиальная тема). Сейчас для меня важнее развеять некоторые расхожие стереотипы о медленной памяти.

  1. Самое главное: медленная память — медленная только во время рисования экрана чипом ULA. Если вы что-то пишете в экран во время отрисовки верхнего или нижнего бордера — не важно где и что вы делаете, любая память для вас работает одинаково. Даже во время рисования бордера слева и справа от экрана медленная память делается нормальной.

    Если с памятью очень плохо и есть эффект, который целиком выполняется во время верхнего или нижнего бордера — его можно положить в медленную память, и код, и данные, разницы на классике не будет. Например, даже если у вас бордер/мультиколор и полный фашизм по тактам, если вы зовёте муз. плейер в начале фрейма — его можно положить в медленную память и всё будет ок.

  2. Немного о природе задержек. Команды Z80 исполняются «кусочками». Z80 сначала берёт код команды из памяти, а потом делает что велит команда, по шагам. К примеру, POP HL выполняется за 3 шага:
    (a) чтение кода команды по адресу PC: 4 такта
    (b) чтение байта по адресу SP: 3 такта
    (c) чтение байта по адресу SP+1: 3 такта
    Если ULA рисует экран, процессор может влезть в память только в один или два конкретных такта на каждые 8 тактов (в зависимости от модели спектрума). Поэтому если положить и стек и сам код в медленную память — шаг (a) (чтение кода) будет ждать пока ULA освободит память, потом шаг (b) будет ждать пока ULA освободит память и потом ещё шагу (c) тоже придётся ждать пока освободится память. Именно поэтому 16K спектрум — это реально нешуточно тормозной компьютер (особенно когда он рисует экран).

    Если у нас какой-то более-менее стандартный код, без особой специальной подгонки, масштабы потерь скорости будут зависеть от вашего стиля работы с экраном. Пусть мы пишем данные в экран байтами, и пусть у нас более-менее случайный доступ (т.е. что-то типа эффекта где рисуется большое кол-во точек). На экране 192 строки, отрисовка каждой из которых занимает 128 тактов процессора, т.е. отображение экрана занимает примерно 192*128 = 24576 тактов, это где-то 35% общего времени в кадре. В среднем, при попытке писать в экран во время работы ULA, ваша команда байтового доступа рискует навлечь на себя 2.625 тактов задержки ((6+5+4+3+2+1+0+0)/8, подробнee — в другой раз). Т.е. в среднем, при случайных доступах к экрану байтовыми командами и кодом, работающим в быстрой памяти, следует ожидать примерно 0.35*2.625 = 0.92 «лишних» такта на каждом байте записанном в экран.

    При работе с двухбайтовыми командами средние задержки могут быть существенно больше. Kонкретный пример: стандартное копирование стеком теневого буфера в экран на 48K машине с медленной памятью, которое не привязано к инту и в итоге «ползёт» по кадру, даёт замедление в среднем где-то на 1.3 такта на каждый скопированный байт (это подразумевая что код и исходный буфер лежат в быстрой памяти). Это типичный пример замедления, при неоптимизированном доступе к медленной памяти, кодом в быстрой памяти, полагающимся на операции со стеком.

    Если учитывать особенности медленной памяти, потери тактов можно существенно уменьшить или даже совсем изничтожить. Если писать не думая о последствиях, можно потерять очень много тактов. Теоретически худший случай — потеря до 30% времени (код в медленной памяти непрерывно тычется в медленную память). Практически, я слышал о потерях до 15-20%. Не будьте одним из этих людей.

  3. Отсюда — правила гигиены. Если вам хочется, чтобы ваш эффект успевал на классике — старайтесь класть код в быструю память. Данные тоже очень желательно по возможности хранить в быстрой памяти. Сам экран находится в медленной памяти, и совсем избежать замедление сложно, поэтому — думайте, как постараться нарисовать побольше во время бордера и м.б. сделать что-то не трогающее медленную память во время отрисовки самого экрана.

Где находится быстрая память и где находится медленная? Ответ на этот вопрос зависит от машины. У 48K компьютеров #4000-#7FFF — медленная память, вся остальная — быстрая. У 128K/+2 «медленными» являются страницы 1,3,5 и 7. Создателям +2A/+2B/+3 не сиделось спокойно на месте, поэтому они сделали медленными страницы 4,5,6 и 7. Таким образом, страницы 0 и 2 — везде быстрые, 5 и 7 — везде медленные. По поводу остальных страниц приходится разбираться по ходу исполнения программы (я расскажу как программно отличить 128K/+2 от +2A/+2B/+3 в следующей части).

В дополнение к медленной памяти стоит упопомянуть, что ввод/вывод в порты тоже бывает медленным; это касается 48K/128K/+2. Более поздние модели, т.е. +2A/+2B/+3 устранили этот недостаток. В частности, медленным является порт #FE, что не учтено ни в одном классическом биперном движке, из-за чего почти все биперные движки звучат объективно чище на спектрумах от Amstrad или на отечественных безвейтовых клонах. Медленными также являются любые порты в адресном пространстве #4000-#7FFF (вы поняли, о каком порте речь), а также порты в окне #C000-#FFFF, если в данное окно в данный момент впечатана страница медленной памяти. Важно: задержки ввода/вывода, точно так же как задержки медленной памяти, проявляются только во время отображения экрана; на бордере никаких задержек нет. В рамках данной серии статей я не собираюсь расписывать задержки ввода/вывода (это слишком редкая, специальная задача). Возможно, я как-нибудь соберусь и расскажу об этих задержках в рамках разговора о биперных движках.

Специально для любителей насиловать железо в особо извращённых формах — вот табличка по неполной дешифрации портов на классических машинах:

ПортМаска для 48KМаска для 128K/+2 Маска для +2A/+2B/+3 
#FExxxxxxxx xxxxxxx0 xxxxxxxx xxxxxxx0 xxxxxxxx xxxxxxx0
#7FFDпорт отсутствует0xxxxxxx xxxxxx0x01xxxxxx xxxxxx0x
#1FFDпорт отсутствуетпорт отсутствует0001xxxx xxxxxx0x
#BFFDпорт отсутствует10xxxxxx xxxxxx0x10xxxxxx xxxxxx0x
#FFFD порт отсутствует11xxxxxx xxxxxx0х11xxxxxx xxxxxx0х
Таблица 3: неполная дешифрация портов на классике.

Примечание: если вам непонятно, что вам пытается сказать эта табличка, я думаю, вам лучше не знать, что вам пытается сказать эта табличка!

продолжение следует.

38 комментариев

avatar
Да! Это как раз тот материал, который я давно хотел собрать и систематизировать для себя. Спасибо:) Еще хотелось бы почитать про растактовку экранной области каждого компа: сколько тактов на линию, сколько в невидимой области бордюра (сверху, снизу, слева и справа), сколько в видимой, сколько до начала экранной области и т.п.

Очень интересно про программные методы определения типа компа, в т.ч. наших клонов (до сих пор не постигаю, например, как реализован универсальный фикс бордюрного эффекта в sage boot (cм.borndead#10) под любую машину)…

И еще: «У 48K компьютеров #4000-#7FFF — медленная память, вся остальная — медленная» — опечатка?
  • bfox
  • +4
avatar
Очень рад что «попал». Растактовка будет во второй части, я базирую на этом некоторые тесты. Детект наших клонов в моём исполнении не вполне идеален, я немного не дописал его в своём просмотрщике бордеров. Но что умею — расскажу.
avatar
И спасибо за замеченную опечатку — исправил!
avatar
о задержках для портами даже не слышал.

реально зоопарк.
так понимаю, в основном ориентироваться стоит на 128K/+2

в следующих частях здорово было-бы рассмотреть стандартный инструментарий для тестов/отладки кода в таких режимах.
ибо эмулей много…
  • VBI
  • +3
avatar
для портов :)
avatar
О, отлично, я об этом даже не думал! :)
avatar
Полезная статья, я раньше не сильно над этим задумывался, зато теперь знаю, как что планировать.
  • ShM
  • +2
avatar
Но надо больше скриншотов/картинок/иллюстраций. Текст давит на мозг. Снег например закриншотить в качестве иллюстрации, как бывает при медленной памяти. И т.п.
avatar
ждём второй серии
avatar
мне тоже интересно — как это, «снег на экране» из-за прерываний в нижней памяти
avatar
ибо влип как-то)
avatar
Картинок много не будет — за этим к бризу пожалуйста :)

Будет код. Жестокий и беспощадный. И гигантские таблицы.
avatar
Ууу. Расходимся, ребята. Картинок не будет.
  • sq
  • +4
avatar
Совсем избаловались! Мультики по телеку вам, а код есть код и пишут его — на коде!!!
avatar
книжка без картинок, не интересная книжка. ©Алиса
avatar
Дыа! Берите пример с моих статей!

avatar
У Алисы котировались книжки с картинками и диалогами.
introspec , добавь хотя бы диалогов?
avatar
Чото смеюсь!
avatar
хороший/плохой пример игрушка FantasyWorldDizzy.
мало того что код плейера/семплов жёстко на 3ей странице
(снег и заторможенное воспроизведение на сером+2),
так ещё и регистр I используется для временного хранения нужных значений.

а вот например в Ruff & Reddy страница для плейера назначается в зависимости от модели спека.
avatar
У меня все демы начинают с того, что грузят модуль определения железа. Вторая часть моей статьи как раз на коде из него и будет основана. Ядро моих дем работает по итогам не с реальными, а с виртуальными номерами страниц, так что я всегда знаю наверняка, какие страницы медленные, а какие — быстрые.
avatar
Профессор! Специально реплаю на ваш реплай с целью добиться внимания.

Первый пункт — нигде в статье капсом или болдом не написано какая именно память медленная. Понятно, что ниже #8000 медленная, а выше быстрая (+нюансы 128К), но это явно нигде в первых строках не написано.

Второй пункт. Я оперирую сейчас 48K моделью и после прочитанного понял, что пытаясь класть что-либо стеком на экран я точно буду тормозить всегда. Далее я опасался того, что если я буду класть стеком на экран и сам код будет ниже #8000 я буду ЕЩЕ БОЛЕЕ тормозить. Однако, практические эксперименты этого не подтверждают — код LD HL,NNNN PUSH HL тормозит (да тормозит) совершенно одинаково будь он в #8000 или в #7000. Этот момент я верно уловил в практическом эксперименте?

Третий пункт. Нет ли у вас на примете эмуляторов, способных включать и выключать (желательно в реальном времени) эффект торможения? Speculator и SpecEMU способны работать в конфигурации с медленной памятью (и именно на их примере я увидел наглядно торможение), но не способны «на ходу» включать и выключать ее.
avatar
Ответ на первый вопрос в третьем абзаце снизу. Ответ на второй как бы содержится в самом статье: задержки зависят от того, где в кадре исполняется код. Подробнее — ну вот оно, сверху, всё написано. Где в кадре выполняется твой код? Если ты сделал что-то типа halt: DUP 100: ld hl,…: push hl: EDUP — неудивительно, что такой код будет работать без задержек даже в медленной памяти. Скажи мне где исполняется твой код, и мне после этого даже объяснять тебе ничего не придётся.

Третий вопрос: то, что тебе хочется, делает ZXSpin. Он не самый точный эмулятор, но я так понимаю, ты ещё не на той стадии, где тебе понадобится 100% точность.
avatar
Да это код вида ld sp,58000: DUP 100: ld hl,…: push hl: EDUP

Да я вижу, что он выполняется медленнее в целом на slow mem машине, но я не вижу разницы во времени его выполнении будь он #8000 или c #6000. Если этой разницы действительно нет, я просто смирюсь с этим. Если же из куска #8000 этот код при такой структуре и таком параметре SP должен быть чуть быстрее — это меняет мои планы.
avatar
Смотри. Важно не только ГДЕ в памяти. Но ещё и КОГДА в кадре. Я в своём примере поставил HALT не случайно, а для того, чтобы код исполнился на верхнем бордере. КОГДА исполняется твой код?
avatar
Переформулирую этот вопрос специально для Макса, чтобы ему было понятнее:

ГДЕ в этот момент находится луч, во время исполнения твоего кода? В какой области бордюра?
avatar
«Дорогие мои старики» sq introspec я все понял про немедленный бордер и другие детали.
Мой код исполняется ВСЕГДА, весь фрейм. Я всегда и бескомпромиссно делаю LD HL,NNNN PUSH HL при SP,#7999
Я пробовал делать это размещая код выше #8000 и ниже #8000, а так же и там и там одновременно. Я вижу что код тормозит по сравнению с Пентагоном, но я не вижу разницы в торможении в зависимости от места нахождения кода (выше или ниже #8000). Все ли я правильно понял про торможение? Если да, я смело размещаю свой код НИЖЕ #8000 так как ничего выиграть в таком сценарии не могу.
avatar
Кстати, после #7999 идёт #799A. Если ты хотел поставить стек перед #8000, то должно быть sp,#7FFF.
avatar
подкалываешь деда… стек в медленной памяти (читай в экране), код LD HL PUSH тоже в медленной памяти, код выполняется весь фрейм, я торможу больше в этой ситуации чем если бы этот код был в быстрой памяти?
avatar
То, что код выполняется весь фрейм — это понятно. Но он в любом случае выполняется последовательно, вместе с лучом. Следовательно, он может замедляться в зависимости от того, где в данный момент времени (во время выполнения кода) находится луч.

То есть, смотри. Произошёл троллинг HALT, твой код начал выполняться, луч в верхнем бордюре. Тут не играет роли, в медленной памяти или в быстрой находится твой код.
В общем, долго ли коротко ли, LD HL PUSH побежал-побежал, и пока всё ровно.

Затем луч пробежал верхний бордюр и добежал до верхней части экрана. Вот тут, если твой код в медленной памяти, то он может незначительно замедляться. А может и нет. Возможно, это вообще не стоит того, чтобы ты сейчас запаривался этим.

Я понимаю, что профессор написал статью привычным для себя, но несколько сложным для абсолютно неподготовленного читателя, языком, и что не всегда бывают силы и время всё это читать и переваривать, для этого, в общем-то, и существуют комментарии и мы. Но я предлагаю тебе сейчас не мудрить, а просто кодить, как кодится. А там дальше разберёшься походу, ну и мы поможем, если что.
avatar
Я беру смелость констатировать, что обсуждаемый код на горизонте целого фрейма ОДИНАКОВО медленно работает (по сравнению с Пентагоном), будь он размещен ниже #8000 или выше #8000. Я вижу это визуально и через контроль PC после IM2 на эмуляторах. Я хотел бы подтверждения этому тезису, чтобы навсегда забыть, смириться «и ебашить как в последний раз» не взирая на место в памяти где лежит мой код.
avatar
Да, это так, ты прав. Подтверждаем; теперь навсегда забудь об этом, смирись, и ебашь, как в последний раз, невзирая на место в памяти и в итоговой таблице результатов конкурса «OTEC INTRO COMPO»!
avatar
Нет, это не так. И это легко проверяется.
org #FFF4
	im 1
	ld sp,(svsp)
	ei 
	ret

	org #FFFF
	jr #FFF4
	
	org #7000 ; 28672
	ld (svsp),sp
	halt
        di 
	ld a,#3A
	ld i,a
	im 2
	ei
	ld bc,0

l1	ld sp,#7999

	ld hl,0
	push hl

	inc bc
	jr l1


	org #8000 ; 32768
	ld (svsp),sp
	halt
        di 
	ld a,#3A
	ld i,a
	im 2
	ei
	ld bc,0

l2	ld sp,#7999

        ld hl,0
	push hl

	inc bc
	jr l2
svsp    dw 0

После ассемблирования PRINT USR 28672 выдает 1114(значение в BC на выходе), а PRINT USR 32768 выдает 1341. Если поставить стек в быструю память (например, #9999), PRINT USR 28672 выдает 1132, а PRINT USR 32768 выдает 1402.
avatar
Результаты получены на 48к модели, на 128х они будут слегка другие, но тенденция та же.
avatar
Естественно это не так, но я сказал то, что он хотел услышать)
avatar
Увеличив количество LD HL,NN:PUSH HL в цикле до 10, получим результаты 230/258. Для стека в быстрой памяти — 236/288.
avatar
убедительно, спасибо
avatar
Слушай, если ты не видишь разницы, это может означать одно из двух. Либо разницы и правда нет, либо она есть, но ты её не замечаешь. Мою точку зрения ты знаешь. Но это твой проект, твоя аудитория и твои решения. У меня совершенно нет ни желания, ни времени тебя в чём-то переубеждать. Я выложил набор некоторых фактов — ты волен ими распоряжаться по твоему собственному усмотрению.
avatar
«Мою точку зрения ты знаешь.» — я как раз пришел ее узнать. Я лишь вел визуальное наблюдение и контролировал через построение одного кадра и снятие показаний SP и PC после прерывания. Постановка эксперимента оказалась не точной. Перепоставил эксперимент. Теперь разницу вижу.

Делаю выводы и фиксирую их. Код вида SP,#5B00 LD HL,NNNN PUSH HL работает медленно будучи размещенным с #8000 и еще чуть медленнее (но уже не критично медленно), будучи размещенным ниже. При учете что код выполняется весь фрейм и мы не думаем о том бордер ли строится нынче или экран.

Для меня это означает невозможность реализации задуманного в чистом виде. Будем реализовывать в грязном.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.