Важная информация
RSS лента

scaraby

Nyan Cat ПК8000

Рейтинг: 2.00. Голосов: 8.
К написанию программы побудил известный видеоролик на Youtube, получивший огромную популярность и набравший гигантское количество просмотров и комментариев. Несмотря на то, что ролик создавался с помощью современных инструментов, идея заключалась в имитации древнего компьютера и 16-цветной анимации.



Это моментально вызвало желание реализовать увиденное, используя компьютер из детства. ПК8000 имеет очень скромные вычислительные ресурсы и весьма своеобразную архитектуру, поэтому, решение такого рода задачи требует определённых усилий в части эффективности создаваемого кода.

Постановка задачи

Видеоролик состоит из 12-ти графических фаз, повторяющихся циклически. Это сопровождается музыкальным сопровождением, состоящим, как минимум, из двух дорожек аккомпанемента и третьей дорожки с вокалом. Предваряет это действие вступление на чёрном фоне, которое воспроизводится единственный раз в начале. Основная мелодия также циклически повторяется до конца ролика, где уводится плавно синхронно с гашением изображения. Реализовать такую насыщенную звуковую картину с помощью лишь одного звукового резонатора едва ли возможно. Параллельно со звуком необходимо постоянно обрабатывать графику. Кроме того, плавный увод графики вряд ли реализуем.

Прикинув свои способности и возможности ПК8000, было принято решение реализовать только музыкальное вступление, графику и одноголосое исполнение мелодии. Прекращение бега кота должно осуществляться по клавише «СТОП». С целью передачи атмосферы романтики трепания нервов загрузкой программ с кассетного магнитофона, конечный результат должен был быть записан на реальную кассету, предпочтительнее, советского производства, типа МК-60-5 или аналогичную. Разумеется, запуск программы должен быть осуществлён на реальном компьютере.

Процесс загрузки программы можно сопроводить выводом на экран заставки по аналогии с загрузкой картинок на ZX-Spectrum.

Воспроизведение звука

Первоначальная попытка решения задачи находилась в плоскости написания изолированных процедур, циклически вызываемых из «карусели событий». Такой подход позволяет создавать более удобоваримый для восприятия исходник. Кроме того, не требуется держать в голове большое количество абстрактных величин при написании очередной процедуры, что заметно отражается на усталости и желании продолжать делать задуманное.

Однако, практика показала, что имеющихся вычислительных ресурсов и опыта программирования недостаточно, чтобы изготовить красивый лаконичный код. Несмотря на то, что прототип программы оказался действующим и скорость смены графических фаз - достаточная для сходства анимации с оригиналом, диапазон звуковых частот оставлял желать лучшего. Удалось воспроизвести только шумовые эффекты, мелодия же получалась слишком фальшивой. Пришлось изменить концепцию и начать программирование с наиболее требовательного к быстродействию участка - звука.

Звук в ПК8000 воспроизводится при помощи порта 82h (бит d7). Инвертируем состояние d7 через промежутки времени, равные половине периода воспроизводимой звуковой волны и получаем «тёплый ламповый» звук встроенного резонатора. Когда тактовая частота процессора и скорость доступа к ОЗУ высокие, то такого рода задача решается легко. На одно прерывание вешается счётчик длительности полупериода, на другое - счётчик длительности звучания ноты. В случае ПК8000 прерывание по таймеру (RST 07h, точка входа 0038h) происходит 1 раз в 20 мс. Оно годится только для измерения длительности звучания самой ноты. Поэтому было принято волевое решение и под воспроизведение звука были полностью отданы регистры d, e и b. В регистре b хранится значение длительности полупериода воспроизводимой ноты, e - счётчик этой длительности, а d - счётчик длительности звучания ноты, минус единица каждые 20 мс. Эти регистры в процессе анимации не используются больше нигде. Блок воспроизведения звука при этом оказался весьма простым:

Код:
	dcr e		;счётчик времени полупериода текущей ноты
	jnz Snd01	;ещё рано, выход из процедуры

	in 82h		;читаем состояние порта
	xri 80h		;инвертируем звуковой бит d7
	out 82h		;записываем новое значение в порт

	mov e,b		;инициализация счётчика
Snd01:
Несмотря на такие меры, попытка оформления этого фрагмента в виде подпрограммы с вызовом через call или rst потерпела неудачу: обращение к стеку в процессе выполнения вызова существенно снижает максимально доступную верхнюю частоту и заметно увеличивает диапазон изменения частоты при изменении параметра счётчика длительности полупериода волны. Решение оказалось ужасным с точки зрения читабельности кода: фрагменты воспроизведения звука были равномерно распределены по тексту программы. Экспериментально было выяснено, что для сносного воспроизведения мелодии достаточно 19-ти таких фрагментов. Дальнейшее увеличение количества фрагментов приводило к слишком медленному изменению графических фаз и кот бежал слишком медленно. Другим ограничением является использование аккумулятора, поэтому необходимо выбирать участки программы, где аккумулятор не используется. Очевидно, что чем более равномерно будет происходить изменение состояния порта, тем чище будет звуковой тон.

Максимально возможной равномерности добиться не удалось по причине того, что обращение к порту возможно только через аккумулятор. Сохранять аккумулятор в стек нельзя, потому что эти команды катастрофически жрут время, максимальная доступная частота звука падает неприемлемо низко и шаг изменения частоты не позволяет точно попадать в гармонию. Пришлось пожертвовать чистотой тона ради достоверности мелодии. Фрагменты воспроизведения звука вставлялись в основную программу в местах, где не требуется сохранять значение в аккумуляторе.

Далее нужно было считать время звучания нот. Для этой цели был привлечён таймер - аппаратное прерывание с частотой 50 Гц, вызываемое сигналом ОБР.ХОД. Имеющийся в ПЗУ обработчик этого прерывания не понравился, поэтому было принято решение программировать векторы прерывания самостоятельно.

Логично, что при записи векторов прерываний в ОЗУ обращение к ПЗУ будет невозможно по причине соответствующей конфигурации адресного пространства, выполняемого путём записи соответствующего значения в порт 80h. В программе используется только две процедуры из ПЗУ: установка цвета и выбор режима. Разумеется, что эти процедуры вызываются до программирования порта 80h.

Когда разрешено аппаратное прерывание и приходит запрос на его выполнение, то устанавливается флаг запрета прерываний, значение счётчика команд сохраняется в стек, а (в ПК8000) в счётчик команд записывается значение 0038h. В ПЗУ по этому адресу записан jmp 24d9h, далее происходит call 0f86eh, где уже пользователь должен разместить jmp на свой обработчик прерывания. В сложившихся условиях тратить столько времени впустую слишком жирно, поэтому обработчик прерывания было решено разместить прямо в ОЗУ по адресу 0038h. В режиме 2 по этому адресу находится массив графики видео ОЗУ и на экране появился характерный мусор. Но этот мусор можно легко скрыть, задав одинаковый цвет фона и изображения. Кроме того, в данном случае на этот участок экрана приходится движущаяся белая звезда на синем фоне, поэтому едва ли его кто-то заметит, если его заранее об этом не предупредить.

Преимуществом такого подхода является тот факт, что после возникновения запроса прерывания и входа в него выполняется минимум дополнительных команд. Прерывания происходят с частотой 50 Гц и не синхронно с фрагментами звуковоспроизведения, что приводит к появлению характерной «наводки» на всей мелодии. Степень различимости на слух этой наводки зависит от того, сколько времени будет возиться обработчик. Именно по этой причине для счётчика длительности звучания ноты был выделен целый отдельный регистр d. Когда же звучание ноты подходит к концу, то время перестаёт быть критичным, потому что миллисекундные паузы между нотами практически неразличимы.

Таким образом, при входе в прерывание, первым делом сохраняем флаги, затем декрементируем регистр d, отвечающий за длительность ноты и, если он не равен нулю, восстанавливаем флаги и пулей из прерывания обратно в основную программу. Если же счётчик обнулился, то проводим проверку на предмет окончания мелодии, в случае её окончания повторно инициализируем указатель нот на первую ноту мелодии, далее загружаем значения длительностей полупериода и времени звучания очередной ноты из ОЗУ и записываем их в регистры b, e и d. Указатель текущей ноты также смещается на следующую по порядку ноту.

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

Обработчик прерываний писался в отдельном окне, а затем переносился в основной текст. При этом вместо ссылок вручную вносились абсолютные значения адресов в ОЗУ. Такой подход позволил разместить обработчик в исходнике в виде текста, а не готового машинного кода. Время на возню с ссылками было потрачено ради увеличения читабельности кода.

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



Кот

Самый первый этап создания программы заключался в извлечении графических данных из видеоролика. В силу того, что самая короткая дорога - та, которую знаешь, в Excel была создана книга с клеточками, которые были закрашены в нужные цвета. Фазы брались прямо из Youtube. Скорость воспроизведения - минимальная, два двойных щелчка на кнопке «Play/Pause» давали смену кадра. Далее кадр перерисовывался в таблицу в Excel. Таким образом, путём заката Солнца вручную, были получены исходные данные о графике.



Следует отметить, что таким образом было получено изображение только кота. Звёздное небо не входило в этот этап. Вариант прорисовки всего кадра целиком был отброшен сразу, как неоптимальный.

Сразу же было выявлено несоответствие: размер кадра в оригинальном ролике состоит из 64х64 квадратика со стороной в 4 пикселя, что соответствует разрешению 256х256 пикселей. ПК8000 имеет разрешение 256х192 пикселя. Решение проблемы - кота сориентировать по вертикали в центре экрана, а расстояние между летящими звёздами уменьшить пропорционально.

Для вывода графики в программе используются две области адресного пространства видео ОЗУ графического режима (SCREEN 2). Первая содержит информацию об изображении/фоне, вторая - о цвете изображения и фона.

Изображение формируется следующим образом. Отдельные точки сгруппированы горизонтально по 8 штук. Точка имеет два логических состояния: 1 - изображение, 0 - фон. На каждую группу расходуется два байта ОЗУ. Первый байт - данные об изображении, второй байт, адрес которого смещён на 2000h, определяет цвет группы из 8 пикселей таким образом, что младший полубайт - цвет изображения, старший полубайт - цвет фона. Таким образом, чтобы получить доступ к отдельной точке, необходимо изменить соответствующий бит в первом байте, не трогая цвет. Следует помнить, что изменение цвета одной точки (второй байт) приводит к изменению цвета всей группы.



Из восьми таких групп пикселей формируется условное знакоместо размером 8х8 пикселей. Это определение удобно для понимания логики адресации графических данных в видео ОЗУ. Изображение в графическом режиме представляет собой массив из таких знакомест, расположенных в порядке слева-направо-сверху-вниз. То есть 32 знакоместа в строке, 24 строки. Исходя из такого расположения и происходит адресация в ОЗУ. Например, если нужно погасить точку с координатами х=9, y=2, то адрес нужного байта будет 000ah, а номер бита d6. Для точки с координатами x=3, y=9 - адрес байта 0101h, бит d4.Так как строка знакомест содержит 32х8=256 байт, то старший байт адреса ОЗУ однозначно является номером соответствующей строки знакомест. Данные особенности учитывались при программировании процедуры прорисовывания кота, как признак окончания строки.

Изображение кота формируется из квадратиков со стороной в 4 пикселя. Соответственно одно знакоместо вмещает в себя четыре квадратика. Это означает, что в массиве графики могут быть только 4 варианта данных: 00h, 0fh, 0f0h и 0ffh. Эти данные сопровождаются атрибутами цвета фона и изображения в массиве цвета. Для того, чтобы получить необходимое изображение, не обязательно в массиве графики использовать все четыре варианта, достаточно одного из двух значений 0fh или 0f0h.



Заполняем весь массив графики значениями 0fh. Получаем на экране вертикальные полосы шириной 4 пикселя. В этом случае для формирования любого изображения из квадратиков 4х4 пикселя достаточно внести нужные значения в массив цвета. При этом за цвет двух соседних квадратиков внутри знакоместа отвечают четыре байта, которые всегда равны друг другу. Таким образом, для кодирования одного знакоместа, содержащего четыре квадратика, достаточно двух байт.

Весь спрайт, содержащий изображение кота, разбивается на строки знакомест. В каждом знакоместе для четырёх квадратиков прописываются значения цвета в 16-ричном виде. Результатом этой работы является таблица, заливка ячеек которой соответствует нужному цвету, а значение равно 16-ричному коду этого цвета по стандарту ПК8000. Первое знакоместо - два байта, затем идут два байта данных о цвете четырёх квадратиков внутри знакоместа справа от текущего и так далее до конца строки, далее следует строка знакомест, расположенная ниже текущей. Далее из этих значений при помощи текстовых формул Excel создаётся строка с данными, пригодными для вставки в исходник.



Прорисовка текущей фазы кота выполняется отдельной процедурой DrawSprite, основная идея которой состоит в переносе данных из области ОЗУ, где хранятся «упакованные» упорядоченные данные о цвете очередного знакоместа, в область видео ОЗУ, именуемой «массив цвета». Как упоминалось выше, необходимости изменять массив графики - нет. Данные о цвете в ОЗУ упорядочены по принципу от младших адресов к старшим, однако, что касается порядка следования графических фаз - фазы с меньшим порядковым номером располагаются в более старших адресах. Такой порядок компоновки данных позволил использовать единый указатель, который декрементируется без проверки. Количество байт, извлекаемых из ОЗУ, определяется количеством строк знакомест и количеством знакомест в строке. В основной программе внутри карусели событий имеется счётчик фаз, обнуление которого приводит к инициализации указателя на последний байт области ОЗУ, содержащий данные о графических фазах.

В силу того, что половина регистров процессора занята звуком, пересылка данных организована с использованием стека. Адрес видео ОЗУ помещается в стек. Адрес ОЗУ записывается в hl, данные из памяти копируются в регистр c, затем командой xthl изменяется вершина стека и hl и данные из c записываются в видео ОЗУ. В процессе записи данных в видео ОЗУ, декрементируется регистр l, что позволяет использовать флаг нуля ZF, как признак окончания строки знакомест. По окончании цикла счёта строк знакомест указатель на область ОЗУ с графикой сохраняется. Он указывает точно на последний байт следующей фазы.

Звёздное небо

Процесс формирования на экране изображения кота заключается в сплошном заполнении области видео ОЗУ новыми данными поверх старых, поэтому не требуется хранение и восстановление предыдущего состояния ячеек памяти, чего не скажешь о перемещающихся по экрану белых звёздах.

Исходя из 12 графических фаз или кадров в исходном видеоролике, состояние звёздного неба также представляет собой 12 блоков с данными, содержащими информацию, сходную по смыслу с данными об изображении кота, но отличающиеся по упорядочиванию.



Звёздное небо для каждой фазы было решено прорисовывать «поквадратно». Всё изображение условно было разбито на пары из двух квадратов в границах знакоместа. Каждое знакоместо содержит две таких пары: верхнюю и нижнюю. Затем были определены адреса видео ОЗУ для каждой такой пары. Изображение внутри такой пары имеет 4 возможных состояния: оба квадрата синие, оба белые, белый-синий и синий-белый. Исходя из таких начальных условий, для каждой фазы имеется группа данных: количество «звёзд» в текущем небе, цвет 1-й звезды, её адрес, цвет 2-й звезды, адрес и т.д. до последнего элемента.

Процедура прорисовки звёздного неба, в отличие от кота, не использует медленные стековые операции. Адрес видео ОЗУ для каждой звезды или пары рядом стоящих звёзд свой, поэтому, указатель на данные о звёздах в ОЗУ сохраняется командами shld и lhld, при этом загрузка в hl адреса видео ОЗУ осуществляется более быстрыми mov. Загружается младший полубайт адреса в acc, затем старший полубайт загружается в h, а после этого из аккумулятора младший полубайт переносится в l.

Порядок расположения фаз звёздного неба в ОЗУ аналогичный порядку расположения фаз кота, что позволяет для повторной инициализации указателей и счётчиков использовать одинаковые признаки.

В отличие от кота, звёздное небо требует восстановления фоновой картинки, которая, к счастью представляет собой сплошное синее поле. Это означает, что значение цвета для записи в видео ОЗУ для всех стираемых звёзд одинаковое и равно 44h.

Процедура стирания звёздного неба идентична процедуре рисования, за исключением того, что данные о цвете не используются и процедура имеет собственный указатель на данные в ОЗУ.

Порядок вызова процедур следующий: рисуются звёзды, рисуется кот, стираются звёзды. Такой порядок позволяет обеспечить время, необходимое для исключения мерцания звёзд. Время рисования/стирания звёздного неба существенно ниже, чем время, необходимое для изображения кота. При этом значение скважности оказывается достаточным для обеспечения необходимой яркости звёзд.

Вступление

В исходном видеоролике сначала проигрывается вступление на чёрном фоне, а затем синхронно с началом основной мелодии появляется изображение. Вступление проигрывается только один раз, основная мелодия циклически повторяется.

Решить задачу однократного воспроизведения вступления удалось следующим образом. Переменная NoteCounter предназначена для определения окончания мелодии, после чего происходит повторная инициализация указателя на текущую ноту и самого счётчика. Анализ состояния счётчика происходит внутри обработчика прерывания таймера, тогда как первичная инициализация - в начале программы вне карусели событий.

Таким образом в начале программы адрес текущей ноты и количество нот мелодии задаётся с учётом вступления, а внутри обработчика прерывания эти параметры указывают на начало основной мелодии и количество нот определяется за вычетом вступления.

Включение изображения представлялось реализовать с помощью сигнала ГАШ (порт 86h, бит d4), однако, на реальном компьютере выяснилось, что этот сигнал вместо простого и понятного отключения RGB от выходов используется для отключения потока светящихся точек. Другими словами, вместо того, чтобы выключить экран, сделав его чёрным, этот сигнал убирает изображение, оставляя фон, что существенно сужает его применимость до режима 0, потому что в режиме 2 экран превращается в набор цветных квадратиков, а в режиме 1 происходит срыв синхронизации и изображение остаётся видимым. Это различие было выявлено лишь на этапе тестирования на реале, когда уже была готова аудиокассета с программой.

В итоге ничего другого не пришло в голову, кроме заливки таблицы цвета синим. Для этого нужно записать в ОЗУ 6 кбайт. Вариант проиграть вступление, осуществить заливку и начать воспроизведение мелодии не прошёл, потому что пауза в мелодии оказалась катастрофической. Пришлось думать над склейкой.

Было принято решение осуществлять заливку прямо во время воспроизведения вступления. Для этого была создана отдельная процедура и введён признак видимости изображения. Переменная Visibility принимала ненулевое значение в момент окончания вступления. Этот признак гарантирует однократный вызов процедуры заливки фона. Вызов процедуры осуществляется из прерывания по таймеру и происходит в моменты окончания воспроизведения мелодии. Когда происходит первый вызов, то процедура выполняется полностью, а переменная Visibility принимает ненулевое значение, что при последующих вызовах процедуры заливки приводит к моментальному выходу из неё.

В процедуру заливки экрана вставлен элемент звуковоспроизведения и подобрана временная задержка с целью выравнивания частоты звучания нот, поэтому скорость заливки не максимальная из возможной, но оптимизация выполнена по критерию достоверности звука, так как искажение звука более неприятно воспринимается, чем «тормознутость» изображения.
Экспериментально было выявлено, что полностью заливка осуществляется в течение звучания четырёх нот, поэтому первоначальная инициализация счётчика количества нот мелодии была выполнена с коррекцией на четыре ноты. Внутри процедуры осуществляется однократная инициализация счётчика основной мелодии, сдвинутая на 4 ноты в область вступления. Другими словами, основная мелодия в первый раз проигрывается с добавлением четырёх нот вступления. В дальнейшем мелодия зацикливается штатно.

Также особенностью данной процедуры является то, что выход из неё осуществляется не в прерывание, из которого она была вызвана, а в основную программу, из которой было вызвано прерывание. Вместо использования команды извлечения данных из стека pop были использованы два inx sp. Оптимизация должна быть во всём.

Следует отметить ещё одну важную роль переменной Visibility. Во все процедуры, записывающие данные в массив графики (рисование звёзд, стирание звёзд и рисование кота) были добавлены команды логического умножения данных о цвете и переменной Visibility. Поэтому, пока играется вступление, то запись данных в массив цвета происходит, но значения нулевые, поэтому пользователь ничего на экране не видит.

Заставка при загрузке

Для того, чтобы сделать загрузку программы с кассеты более динамичной и интересной, была взята идея загрузки на экран заставки по типу Спектрума. Несмотря на почти одинаковое разрешение, объём ОЗУ, занимаемый ПК8000 графикой, составляет 12 кбайт. Причина этого в том, что объём ОЗУ, содержащий информацию о цвете одного знакоместа составляет 8 байт, тогда как у Спектрума - 1 байт. Но и это ещё не всё. Массив цвета расположен не сразу за массивом графики, а через 512 байт, содержащих так называемый буфер экрана, предназначенный для вывода текста.
Из-за этой особенности пришлось заставку загружать в два этапа: сначала грузим изображение, после чего грузим цвет. Но прежде, чем приступать к загрузке, необходимо сформировать само изображение, чтобы было, что загружать.

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



Затем в программе ZX Spectrum screen editor картинка была преобразована к формату спектрумовского изображения scr и экспортирована в bmp-файл с 4-битным разрешением.



После этого полученный файл был загружен в редактор Paint и доработан в нём к виду, который мог быть отображён на экране ПК8000.



Полученный итоговый файл был преобразован при помощи отдельно написанной утилиты к виду, пригодному для загрузки непосредственно в Видео ОЗУ ПК8000 и выгружен в формате WAV в виде отдельного блока данных. Точнее двух блоков данных: один WAV файл содержал данные об изображении, второй - о цвете.

Таким образом, с учётом блока основной программы, получилось три фрагмента данных в формате WAV. Осталось только написать загрузчик, который бы эти файлы мог прочитать с ленты и загрузить в нужные адреса ОЗУ.

Загрузчик

Для изготовления загрузчика вполне подходила процедура чтения с ленты, встроенная в ПЗУ, но, чтобы прибавить аутентичности и динамики, возникла идея добавить команду, отправляющую считанный байт в порт 88h, отвечающий за цвет рамки. Это приводит к тому, что рамка мигает разными цветами во время загрузки.

Опасения о том, что загрузка может перестать работать, если изменятся временные характеристики из-за добавления команды, не подтвердились. Стандартный загрузчик был извлечён из ПЗУ, смещён в адресном пространстве и добавлен в текст собственного загрузчика в виде отдельной процедуры. Это, правда, привело к несовместимости с эмулятором EMU. Cas версия программы не работает в эмуляторе, но вся соль динамической загрузки заставки на реальном ПК8000 и состоит в прослушивании характерного звука и наблюдением за формирующимся синхронно изображением на экране.



После окончания написания загрузчика и тестирования его с использованием сформированных WAV модулей осталось самую малость - записать эти WAV в нужном порядке на магнитную ленту. Как известно, чтобы такие данные оказались читабельными, лучше всего в качестве источника использовать непосредственно выход ПК8000, но если использовать для записи команду BSAVE, то мы получаем на ленте блок данных, в начале которого идут шесть байт, содержащих данные об адресах начала, конца и запуска. Решить эту задачу можно путём вызова только части процедуры, записывающей на ленту конкретное количество байт.

Для облегчения себе жизни было принято решение написать копировщик. К тому же, этот инструмент был необходим для оцифровки большого количества программ к ПК8000, хранящихся на аудиокассетах и имеющих встроенные средства защиты от копирования.

Написанная для этих целей утилита получила название COPY10, корни которого имеют 15-летнюю историю. В 2001 году уже был изготовлен копировщик с меньшей функциональностью. По мере работы над ним результаты сохранялись на ленте, а номера в названии присваивались, чтобы облегчить поиск последней версии. Окончательный вариант в те времена оказался под именем COPY09, поэтому для новой улучшенной и умной версии было выбрано имя COPY10.

Хотя написание копировщика заняло меньшее время, чем описываемая программа, но объём проделанной работы был таков, что его хватит для написания ещё одной статьи.

P.S: Результаты работы были опубликованы в Youtube.

Обновлено 10.11.2016 в 10:31 scaraby

Метки: Нет Добавить / редактировать метки
Категории
Без категории

Комментарии

  1. Аватар для creator
    Интересненько, но картинки из вконтакта не отображаются.
  2. Аватар для scaraby
    Цитата Сообщение от creator
    Интересненько, но картинки из вконтакта не отображаются.
    Спасибо! Поправил картинки с помощью radikal.ru.

Трекбэков