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, который позволяет менять расклад спектрумовской памяти весьма кардинальным образом. Вот расклад этого порта по битам:
Таблица 1: раскладка порта #1FFD.
Что реально делают биты 3 и 4 я не знаю и знать не хочу. Куда более интересны тут ненормальные режимы адресации. Выглядят они так (каждая строка начинается с состояния битов 2 и 1 порта #1FFD):
Таблица 2: ненормальные режимы адресации для +2A/+2B/+3.
Экраны, как всегда, прибиты к страницам 5 и 7, таким образом, в конфигурации 00 у вас нет ни одного экрана, а в конфигурации 01 — их сразу 2. Т.к. чуть более старые спектрумы (128К и +2) не поддерживают порт #1FFD, на практике, очень мало какой-то софт использует эти дополнительные возможности по организации памяти. Но, если вы написали ваш софт на Скорпионе или Кае или одном из их родственников, есть ненулевая возможность что ваш софт полезет в порт #1FFD и попадёт в беду. Старые диспетчеры расширенной памяти из спектрум-прессы это не учитывают — будьте бдительны!
Вторая проблема при работе с памятью на классике заключается в том, что половина вашей памяти — «медленная»! Насколько медленная? смотря что с ней делать. Я хочу подробно описать методику расчёта тормозов медленной памяти во третьей части моей статьи (это отдельная большая и нетривиальная тема). Сейчас для меня важнее развеять некоторые расхожие стереотипы о медленной памяти.
Где находится быстрая память и где находится медленная? Ответ на этот вопрос зависит от машины. У 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, если в данное окно в данный момент впечатана страница медленной памяти. Важно: задержки ввода/вывода, точно так же как задержки медленной памяти, проявляются только во время отображения экрана; на бордере никаких задержек нет. В рамках данной серии статей я не собираюсь расписывать задержки ввода/вывода (это слишком редкая, специальная задача). Возможно, я как-нибудь соберусь и расскажу об этих задержках в рамках разговора о биперных движках.
Специально для любителей насиловать железо в особо извращённых формах — вот табличка по неполной дешифрации портов на классических машинах:
Таблица 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 | строб порта принтера |
Что реально делают биты 3 и 4 я не знаю и знать не хочу. Куда более интересны тут ненормальные режимы адресации. Выглядят они так (каждая строка начинается с состояния битов 2 и 1 порта #1FFD):
Бит 2 | Бит 1 | Страницы памяти (снизу вверх, т.е. страница с адреса #0000, страница с адреса #4000 и т.д.) |
0 | 0 | память составлена из страниц 0,1,2,3 |
0 | 1 | память составлена из страниц 4,5,6,7 |
1 | 0 | память составлена из страниц 4,5,6,3 |
1 | 1 | память составлена из страниц 4,7,6,3 |
Экраны, как всегда, прибиты к страницам 5 и 7, таким образом, в конфигурации 00 у вас нет ни одного экрана, а в конфигурации 01 — их сразу 2. Т.к. чуть более старые спектрумы (128К и +2) не поддерживают порт #1FFD, на практике, очень мало какой-то софт использует эти дополнительные возможности по организации памяти. Но, если вы написали ваш софт на Скорпионе или Кае или одном из их родственников, есть ненулевая возможность что ваш софт полезет в порт #1FFD и попадёт в беду. Старые диспетчеры расширенной памяти из спектрум-прессы это не учитывают — будьте бдительны!
Вторая проблема при работе с памятью на классике заключается в том, что половина вашей памяти — «медленная»! Насколько медленная? смотря что с ней делать. Я хочу подробно описать методику расчёта тормозов медленной памяти во третьей части моей статьи (это отдельная большая и нетривиальная тема). Сейчас для меня важнее развеять некоторые расхожие стереотипы о медленной памяти.
- Самое главное: медленная память — медленная только во время рисования экрана чипом ULA. Если вы что-то пишете в экран во время отрисовки верхнего или нижнего бордера — не важно где и что вы делаете, любая память для вас работает одинаково. Даже во время рисования бордера слева и справа от экрана медленная память делается нормальной.
Если с памятью очень плохо и есть эффект, который целиком выполняется во время верхнего или нижнего бордера — его можно положить в медленную память, и код, и данные, разницы на классике не будет. Например, даже если у вас бордер/мультиколор и полный фашизм по тактам, если вы зовёте муз. плейер в начале фрейма — его можно положить в медленную память и всё будет ок.
- Немного о природе задержек. Команды 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%. Не будьте одним из этих людей.
- Отсюда — правила гигиены. Если вам хочется, чтобы ваш эффект успевал на классике — старайтесь класть код в быструю память. Данные тоже очень желательно по возможности хранить в быстрой памяти. Сам экран находится в медленной памяти, и совсем избежать замедление сложно, поэтому — думайте, как постараться нарисовать побольше во время бордера и м.б. сделать что-то не трогающее медленную память во время отрисовки самого экрана.
Где находится быстрая память и где находится медленная? Ответ на этот вопрос зависит от машины. У 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 |
#FE | xxxxxxxx xxxxxxx0 | xxxxxxxx xxxxxxx0 | xxxxxxxx xxxxxxx0 |
#7FFD | порт отсутствует | 0xxxxxxx xxxxxx0x | 01xxxxxx xxxxxx0x |
#1FFD | порт отсутствует | порт отсутствует | 0001xxxx xxxxxx0x |
#BFFD | порт отсутствует | 10xxxxxx xxxxxx0x | 10xxxxxx xxxxxx0x |
#FFFD | порт отсутствует | 11xxxxxx xxxxxx0х | 11xxxxxx xxxxxx0х |
Примечание: если вам непонятно, что вам пытается сказать эта табличка, я думаю, вам лучше не знать, что вам пытается сказать эта табличка!
продолжение следует.
38 комментариев
Очень интересно про программные методы определения типа компа, в т.ч. наших клонов (до сих пор не постигаю, например, как реализован универсальный фикс бордюрного эффекта в sage boot (cм.borndead#10) под любую машину)…
И еще: «У 48K компьютеров #4000-#7FFF — медленная память, вся остальная — медленная» — опечатка?
реально зоопарк.
так понимаю, в основном ориентироваться стоит на 128K/+2
в следующих частях здорово было-бы рассмотреть стандартный инструментарий для тестов/отладки кода в таких режимах.
ибо эмулей много…
Будет код. Жестокий и беспощадный. И гигантские таблицы.
introspec , добавь хотя бы диалогов?
мало того что код плейера/семплов жёстко на 3ей странице
(снег и заторможенное воспроизведение на сером+2),
так ещё и регистр I используется для временного хранения нужных значений.
а вот например в Ruff & Reddy страница для плейера назначается в зависимости от модели спека.
Первый пункт — нигде в статье капсом или болдом не написано какая именно память медленная. Понятно, что ниже #8000 медленная, а выше быстрая (+нюансы 128К), но это явно нигде в первых строках не написано.
Второй пункт. Я оперирую сейчас 48K моделью и после прочитанного понял, что пытаясь класть что-либо стеком на экран я точно буду тормозить всегда. Далее я опасался того, что если я буду класть стеком на экран и сам код будет ниже #8000 я буду ЕЩЕ БОЛЕЕ тормозить. Однако, практические эксперименты этого не подтверждают — код LD HL,NNNN PUSH HL тормозит (да тормозит) совершенно одинаково будь он в #8000 или в #7000. Этот момент я верно уловил в практическом эксперименте?
Третий пункт. Нет ли у вас на примете эмуляторов, способных включать и выключать (желательно в реальном времени) эффект торможения? Speculator и SpecEMU способны работать в конфигурации с медленной памятью (и именно на их примере я увидел наглядно торможение), но не способны «на ходу» включать и выключать ее.
Третий вопрос: то, что тебе хочется, делает ZXSpin. Он не самый точный эмулятор, но я так понимаю, ты ещё не на той стадии, где тебе понадобится 100% точность.
Да я вижу, что он выполняется медленнее в целом на slow mem машине, но я не вижу разницы во времени его выполнении будь он #8000 или c #6000. Если этой разницы действительно нет, я просто смирюсь с этим. Если же из куска #8000 этот код при такой структуре и таком параметре SP должен быть чуть быстрее — это меняет мои планы.
ГДЕ в этот момент находится луч, во время исполнения твоего кода? В какой области бордюра?
Мой код исполняется ВСЕГДА, весь фрейм. Я всегда и бескомпромиссно делаю LD HL,NNNN PUSH HL при SP,#7999
Я пробовал делать это размещая код выше #8000 и ниже #8000, а так же и там и там одновременно. Я вижу что код тормозит по сравнению с Пентагоном, но я не вижу разницы в торможении в зависимости от места нахождения кода (выше или ниже #8000). Все ли я правильно понял про торможение? Если да, я смело размещаю свой код НИЖЕ #8000 так как ничего выиграть в таком сценарии не могу.
То есть, смотри. Произошёл
троллингHALT, твой код начал выполняться, луч в верхнем бордюре. Тут не играет роли, в медленной памяти или в быстрой находится твой код.В общем, долго ли коротко ли, LD HL PUSH побежал-побежал, и пока всё ровно.
Затем луч пробежал верхний бордюр и добежал до верхней части экрана. Вот тут, если твой код в медленной памяти, то он может незначительно замедляться. А может и нет. Возможно, это вообще не стоит того, чтобы ты сейчас запаривался этим.
Я понимаю, что профессор написал статью привычным для себя, но несколько сложным для абсолютно неподготовленного читателя, языком, и что не всегда бывают силы и время всё это читать и переваривать, для этого, в общем-то, и существуют комментарии и мы. Но я предлагаю тебе сейчас не мудрить, а просто кодить, как кодится. А там дальше разберёшься походу, ну и мы поможем, если что.
После ассемблирования PRINT USR 28672 выдает 1114(значение в BC на выходе), а PRINT USR 32768 выдает 1341. Если поставить стек в быструю память (например, #9999), PRINT USR 28672 выдает 1132, а PRINT USR 32768 выдает 1402.
Делаю выводы и фиксирую их. Код вида SP,#5B00 LD HL,NNNN PUSH HL работает медленно будучи размещенным с #8000 и еще чуть медленнее (но уже не критично медленно), будучи размещенным ниже. При учете что код выполняется весь фрейм и мы не думаем о том бордер ли строится нынче или экран.
Для меня это означает невозможность реализации задуманного в чистом виде. Будем реализовывать в грязном.