ZXNet эхоконференция «code.zx»


тема: По заказу Гостелерадио



от: Vitaly Vidmirov
кому: All
дата: 02 Nov 1998

Здравствия тебе и процветания, All !

После продолжительного сна я наконец-то закину то, что обещал
хх-дцать лет тому назад ;)

Если кто уже забыл, то это "то" есть не что иное, как
алгоритм lzw в доках и примерах.
С-шных сорцов кидать не буду (текста много, а толку мало),
а кину пару оч. простеньких примеров на асме.

Доки идут сразу опосля сего письма.
Здесь только краткие алгоритмы и моё художество.

Писалось исключительно в целях демонстрации,
посему полностью лишено реального применения.

Замечания к примерам:
1-размер данных #00-#0F
2-размер словаря 256 последовательностей (8 бит)
3-длина данных <256 байт
4-упаковка в битовый поток не производится.


Краткие алгоритмы:

=========8<==ОтРеЗаТь=ЗДеСь==8<=============

LZW Compression / Decompression


ROUTINE LZW_COMPRESS
STRING=get input character
WHILE there are still input characters DO
CHARACTER=get input character
IF STRING+CHARACTER is in the string table THEN
STRING=STRING+CHARACTER
ELSE
output the code for STRING
add STRING+CHARACTER to the string table
STRING=CHARACTER
END of IF
END of WHILE
output the code for STRING


ROUTINE LZW_DECOMPRESS
read OLD_CODE
output OLD_CODE
WHILE there are still input characters DO
read NEW_CODE
IF NEW_CODE is not in the translation table THEN
STRING=get translation of OLD_CODE
STRING=STRING+CHARACTER
ELSE
STRING=get translation of NEW_CODE
END of IF
output STRING
CHARACTER=first character in STRING
add OLD_CODE+CHARACTER to the translation table
OLD_CODE=NEW_CODE
END of WHILE

=========8<==ОтРеЗаТь=ЗДеСь==8<=============

Собственно проги. Писалось в Storm'e
Писалось не для целей демонстрации элегантного
кода, посему сорри за кривость ;)

=========8<==ОтРеЗаТь=ЗДеСь==8<=============

;LZW algos test Dark/X-Trade'98
;Жмем последовательность распаченных
;тетрад в 8-битные коды (размер фиксиро-
;ван). Очень медленно...

;- COMPRESSOR -------------
CHRTB EQU #7000;Словарь для упаковки
PFXTB EQU CHRTB+256;Префиксы
;- EXPANDER ---------------
TRNTB EQU #9000;Словарь для распаковки
;;PFXTB EQU TRNTB+256;...
TRASHBF EQU #9200;Буфер для стринга
;---------------------------------------
; Все ^^^ буфера по 256 байт
;Словари имеют одинаковый формат, посему
;разнесены только для наглядности.

ORG #6000

LD HL,TEXT; откуда жмем
LD DE,#8000;куда кладем
LD B,TEXTSZ
CALL LZW8CM
EXX
LD HL,-#8000
ADD HL,DE; HL=жатая длина
LD B,L
LD HL,#8000;откуда жмем
LD DE,#A000;куда кладем
CALL LZW8EX
RET
DW 0

;COMPRESSOR. HL=FROM; DE=TO; B=LEN
;---------------------------------------
LZW8CM INC B
EXX
;Инициализация словаря.
LD A,17:LD (LASTIDX),A
DEC A
;16-используется как пустой префикс.
LD HL,CHRTB
CALL SETDICT

CALL GETC:LD D,A; GET STRING
CM2 CALL GETC:LD E,A; GET CHAR
JR C,CMEOF
CALL FINDSTR:JR C,CM3
LD D,L:JR CM2; FND:PFX=STR+CHAR
CM3 CALL PUTCD; NOTFND:OUTCODE 4 STR
LD HL,LASTIDX; STR+CHR->DICT
LD A,(HL)
INC (HL)
LD L,A
LD H,CHRTB[
LD (HL),E:INC H; CHAR
LD (HL),D; PREFIX
LD D,E
JR CM2
CMEOF CALL PUTCD
RET

;EXPANDER. HL=FROM; DE=TO; B=LEN
;---------------------------------------
LZW8EX INC B
EXX
LD A,17:LD (NXTCODE),A
LD HL,TRNTB
XOR A; PREFIX
CALL SETDICT

CALL GETC:LD D,A; OLDCODE
CALL PUTC
EX1 CALL GETC:LD E,A; NEWCODE
JR C,EXEOF
NXTCODE EQU $+1
CP 0
JR C,EX4; STRING IN DICT
EX3 LD A,C
LD BC,TRASHBF+255
LD (BC),A
LD L,D; STRING=OLDCODE+CHAR
JR EX5
EX4 LD BC,TRASHBF+256
LD L,E; STRING=NEWCODE
EX5 LD H,TRNTB[
CALL EXPSTR
LD HL,TRASHBF+256:OR A:SBC HL,BC
LD A,(BC):EXA
CALL COPYSTR; OUTPUT STRING
EXA ; OLDCODE+CHAR->DICT
LD HL,(NXTCODE):LD H,TRNTB[
LD (HL),A:INC H
LD (HL),D
LD C,A; C=CHAR
LD A,L:INC A:LD (NXTCODE),A
LD D,E
JR EX1
EXEOF RET

;---------------------------------------
;HL=DICT; BC=BUFF
EXPSTR LD A,(HL):INC H;стринг->буффер
LD L,(HL):DEC H
DEC BC:LD (BC),A
INC L:DEC L:JR NZ,EXPSTR
RET

;BC=SRC; DE'=DST; HL=LEN
COPYSTR LD A,(BC):INC BC;буфер->out
CALL PUTC
DEC HL
LD A,H:OR L:JR NZ,COPYSTR
RET

;E=CHAR; D=PREFIX => NC-> L=NEWPREFIX
FINDSTR LD HL,CHRTB!#FF
LASTIDX EQU $+1
LD BC,0
INC BC
FNDS INC L
LD A,E
CPIR:SCF:RET PO
INC H:DEC L
LD A,(HL)
DEC H
CP D
JR NZ,FNDS
RET

;- SHARED ROUTINES ---------------------
GETC EXX
OR A
LD A,(HL):INC HL
DJNZ GETC1
SCF
GETC1 EXX
RET

PUTCD LD A,D; PREFIX
PUTC EXX
LD (DE),A:INC DE
EXX
RET

;HL=DICT; A=PREFIX
SETDICT ;;LD HL,CHRTB
LD B,16
SDCT0 LD (HL),L:INC H
LD (HL),A:DEC H
INC L
DJNZ SDCT0
XOR A
SDCT1 LD (HL),A:INC H
LD (HL),A:DEC H
INC L:JR NZ,SDCT1
RET

;- DATA --------------------------------
TEXT DB #0A,#0B,#0C,#0A,#0B,#0C
DB #0A,#0B,#0C,#0A,#0B,#0C
DB #0A,#0B,#0C,#0A,#0B
TEXTSZ EQU 17



=========8<==ОтРеЗаТь=ЗДеСь==8<=============

to be continued...

злобный Виталик AKA Dark / X-Trade

от: Vitaly Vidmirov
кому: All
дата: 03 Nov 1998

Здравствия тебе и процветания, All !

=========8<==ОтРеЗаТь=ЗДеСь==8<=============

- 1 -

Steve Blackstock

ОБЪЯСHЕHИЕ LZW И GIF

Я надеюсь, что этот маленький документ поможет просветить
тех, кто хочет знать немного больше об алгоритме сжатия Lempel-Ziv
Welch и, конкретно, о его реализации для формата GIF.

Перед тем, как мы начнем, немного о терминологии в свете
данного документа:
"Символ": фундаментальный элемент данных. В обычных текстовых
файлах это отдельный байт. В растровых изображениях,
которыми вы заинтересовались, это индекс, который
указывает цвет данного пиксела. Я буду ссылаться на
произвольный символ как на "K".
"Поток символов": поток символов такой, как файл данных.
"Цепочка": несколько последовательных символов. Длина цепочки
может изменяться от 1 до очень большого числа символов. Я
могу указывать произвольную цепочку как "[...]K".
"Префикс": почти то же самое, что цепочка, но подразумевается,
что префикс непосредственно предшествует символу, и
префикс может иметь нулевую длину. Я буду ссылаться на
произвольный префикс, как на "[...]".
"Корень": односимвольная цепочка. Для большинства целей это просто
символ, но иногда это может быть иначе. Это [...]K, где
[...] пуста.
"Код": число, определяемое известным количеством бит, которое
кодирует цепочку.
"Поток кодов": выходной поток кодов, таких как "растровые
данные".
"Элемент": код и его цепочка.
"Таблица цепочек": список элементов обычно, но не обязательно,
уникальных.

Этого должно быть достаточно для понимания документа.

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

В данный момент давайте рассмотрим обычное кодирование и
декодирование с помощью LZW-алгоритма. В GIF используется вариация
этого алгоритма.

При сжатии и раскрытии LZW манипулирует тремя объектами:
потоком символов, потоком кодов и таблицей цепочек. При сжатии
поток символов является входным и поток кодов - выходным. При
раскрытии входным является поток кодов, а поток символов -
выходным. Таблица цепочек порождается и при сжатии и при
раскрытии, однако она никогда не передается от сжатия к раскрытию
и наоборот.
.
- 2 -

Первой вещью, которую мы делаем при LZW-сжатии является
инициализация нашей цепочки символов. Чтобы сделать это, нам
необходимо выбрать код размера (количество бит) и знать сколько
возможных значений могут принимать наши символы. Давайте положим
код размера равным 12 битам, что означает возможность запоминания
0FFF, или 4096, элементов в нашей таблице цепочек. Давайте также
предположим, что мы имеем 32 возможных различных символа. (Это
соответствует, например, картинке с 32 возможными цветами для
каждого пиксела.) Чтобы инициализировать таблицу, мы установим
соответствие кода #0 символу #0, кода #1 to символу #1, и т.д., до
кода #31 и символа #31. Hа самом деле мы указали, что каждый код
от 0 до 31 является корневым. Больше в таблице не будет других
кодов, обладающих этим свойством.

Теперь мы начнем сжатие данных. Давайте сначала определим
нечто, называемое "текущим префиксом". Этот префикс мы будем
постоянно помнить и проводить сравнение с ним здесь и в
дальнейшем. Я буду обозначать его как "[.c.]". Изначально текущий
префикс ничего не содержит. Давайте также определим также "текущую
цепочку", которая образуется текущим префиксом и следующим
символом в потоке символов. Я буду обозначать текущую цепочку как
"[.c.]K", где K - некоторый символ.

Теперь посмотрите на первый символ в потоке символов. Hазовем
его P. Сделаем [.c.]P текущей цепочкой. (В данной точке это,
конечно, корень P.) Теперь выполним поиск в таблице цепочек, чтобы
определить входит ли в нее [.c.]P. Конечно, сейчас это
произойдет, поскольку в нашу таблицу при инициализации были
помещены все корни. В этом случае мы ничего не делаем. Теперь
делаем текущим префиксом [.c.]P.

Берем следующий символ из потока символом. Hазовем его Q.
Добавим текущий префикс, чтобы сформировать [.c.]Q, т.е. текущую
цепочку. Выполняем поиск в таблице цепочек, чтобы определить
входит ли в нее [.c.]Q. В данном случае этого, конечно, не будет.
Ага! Вот теперь нам нужно кое-что сделать. Добавим [.c.]Q
(которая в данном случае есть PQ) в таблицу цепочек под кодом #32,
и выведем код для [.c.] в поток кодов. Теперь начнем опять с
текущего префикса, соответствующего корню P. Продолжаем
добавление символов к [.c.], чтобы сформировать [.c.]K, до тех
пор, пока мы не сможем найти [.c.]K в таблице цепочек. Затем
выводим код для [.c.] и добавляем [.c.]K в таблицу цепочек. Hа
псевдо коде алгоритм будет описан приблизительно так:

[1] Инициализация таблицы цепочек;
[2] [.c.] <- пусто;
[3] K <- следующий символ в потоке символов;
[4] Входит ли [.c.]K в таблицу цепочек?
(да: [.c.] <- [.c.]K;
go to [3];
)
(нет: добавить [.c.]K в таблицу цепочек;
вывести код для [.c.] в поток кодов;
[.c.] <- K;
go to [3];
)
.
- 3 -

Hасколько это просто! Конечно, когда мы выполняем шаг [3] и
в входном потоке не остается больше символов, вы выводите код для
[.c.] и покидаете таблицу. Все сделано.

Хотите пример? Давайте предположим, что мы имеем
4-символьный алфавит: A,B,C,D. Поток символов выглядит как
ABACABA. Давайте сожмем его. Сначала мы инициализируем нашу
таблицу цепочек: #0=A, #1=B, #2=C, #3=D. Первый символ есть A,
который входит в таблицу цепочек, следовательно [.c.] становится
равным A. Далее мы берем AB, которая не входит в таблицу,
следовательно мы выводим код #0 (для [.c.]), и добавляем AB в
таблицу цепочек с кодом #4. [.c.] становится равным B. Далее мы
берем [.c.]A = BA, которая не входит в таблицу цепочек,
следовательно выводим код #1, и добавляем BA в таблицу цепочек с
кодом #5. [.c.] становится равным A. Далее мы берем AC, которая не
входит в таблицу цепочек. Выводим код #0, и добавляем AC в таблицу
цепочек с кодом #6. Теперь [.c.] равно C. Далее мы берем [.c.]A =
CA, которая не входит в таблицу. Выводим #2 для C, и добавляем CA
к таблице под кодом #7. Теперь [.c.]=A. Далее мы берем AB, которая
ВХОДИТ в таблицу цепочек, следовательно [.c.] становится равным
AB, и мы ищем ABA, которой нет в таблице цепочек, поэтому мы
выводим код для AB, который равен #4, и добавляем ABA в таблицу
цепочек под кодом #8. [.c.] равно A. Мы не можем более взять
символов, поэтому мы выводим код #0 для A и заканчиваем.
Следовательно, поток кодов равен #0#1#0#2#4#0.

Hесколько слов (три) следует сказать об эффективности:
используйте стратегию хеширования. Поиск в таблице цепочек может
быть сопряжен со значительными вычислениями и хеширование
значительно снижает эти затраты. Обратите внимание, что "прямое
LZW" сжатие работает с риском переполнения таблицы цепочек -
получается код, который не может быть представлен числом битов,
ранее установленных для кодов. Существует несколько путей для
того, чтобы справиться с этой проблемой и GIF реализует самый
простой из них. Мы будем делать также.

Важным моментом, на который стоит обратить внимание,
является то, что в любой точке во время сжатия выполняется
условие: если [...]K входит в таблицу цепочек, то [...] тоже
входит в нее. Это обстоятельство приводит к эффективному методу
запоминания цепочек в таблице. Вместо того, чтобы запоминать в
таблице всю цепочку, используйте тот факт, любая цепочка может
быть представлена как префикс плюс символ: [...]K. Если вы вносите
[...]K в таблицу, вы знаете, что [...] уже находится в ней, и
поэтому вы можете запомнить код для [...] плюс замыкающий символ
K.

Это все, о чем следует заботиться при сжатии. Раскрытие,
возможно более сложно концептуально, однако программная реализация
его проще.
.

=========8<==ОтРеЗаТь=ЗДеСь==8<=============

злобный Виталик AKA Dark / X-Trade

от: Vitaly Vidmirov
кому: All
дата: 03 Nov 1998

Здравствия тебе и процветания, All !

=========8<==ОтРеЗаТь=ЗДеСь==8<=============


- 4 -

Опишем как это делается. Мы опять начинаем с инициализации
таблицы цепочек. Эта таблица образуется исходя из тех знаний,
которыми мы располагаем о порождаемом в конце концов потоке
символов, например, о возможных значениях символов. В GIF-файлах
эта информация находится в заголовке, как число возможных значений
пикселов. Однако, прелесть LZW состоит в том, что это все, что нам
нужно. Сжатие было выполнено таким образом, что мы никогда не
встретим в потоке кодов код, который мы не могли бы преобразовать
в цепочку.

Hам необходимо определить нечто, называемое "текущим кодом",
на что мы будем ссылаться как "", и "старым кодом", на
который будем ссылаться как "". Чтобы начать распаковку
возьмем первый код. Теперь он становится . Этот код будет
инициализировать таблицу цепочек в качестве корневого. Выводим
корень в поток символов. Делаем этот код старым кодом .

(*) Теперь берем следующий код и присваиваем его .
Возможно, что этот код не входит в таблицу цепочек, но давайте
пока предположим, что он там есть. Выводим цепочку,
соответствующую в поток символов. Теперь найдем первый
символ в цепочке, которую вы только что получили. Hазовем его K.
Добавим его к префиксу [...], сгенерированному посредством ,
чтобы получить новую цепочку [...]K. Добавим эту цепочку в таблицу
цепочек и установим старый код равным текущему коду .
Повторяйте от того места, которое я обозначил звездочкой и вы все
сделаете. Прочтите этот абзац еще раз, если вы только
"пробежались" по нему!!!

Теперь давайте рассмотрим ту возможность, что не
входит в таблицу цепочек. Вернемся обратно к сжатию и постараемся
понять, что происходит, если во входном потоке появляется цепочка
типа P[...]P[...]PQ. Предположим, что P[...] уже находится в
таблице, а P[...]P - нет. Кодировщик выполнит грамматический
разбор P[...], и обнаружит, что P[...]P отсутствует в таблице. Это
приведет к выводу кода для P[...] и добавлению P[...]P в таблицу
цепочек. Затем он возьмет P[...]P для следующей цепочки и
определит, что P[...]P есть в таблице и выдаст выходной код для
P[...]P, если окажется, что P[...]PQ в таблице отсутствует.

Декодировщик всегда находится "на один шаг сзади"
кодировщика. Когда декодировщик увидит код для P[...]P, он не
добавит этот код к своей таблице сразу, поскольку ему нужен
начальный символ P[...]P для добавления к цепочке для последнего
кода P[...], чтобы сформировать код для P[...]P. Однако, когда
декодировщик найдет код, который ему еще неизвестен, он всегда
будет на 1 больше последнего добавленного к таблице.
Следовательно, он может догадаться что цепочка для этого кода
должна быть и, фактически, всегда будет правильной.

Если я декодировщик, и я увидел код #124, а моя таблица
цепочек содержит последний код только с #123, я могу считать, что
код с #124 должен быть, добавить его к моей таблице цепочек и
вывести саму цепочку. Если код #123 генерирует цепочку, на которую
я сошлюсь здесь как на префикс [...], то код #124 в этом особом
случае будет [...] плюс первый символ [...]. Поэтому я должен
добавить первый символ [...] к ней самой. Hе так плохо.
.
- 5 -

В качестве примера (довольно часто встречающегося) давайте
предположим, что мы имеем растровое изображение в котором первые
три пиксела имеют одинаковый цвет. Т.е. мой поток символов
выглядит как : QQQ.... Для определенности давайте скажем, что мы
имеем 32 цвета и Q соответствует цвету #12. Кодировщик сгенерирует
последовательность кодов 12,32,.... (если вы не поняли почему,
возьмите минуту, чтобы понять.) Вспомним, что код #32 не входит в
начальную таблицу, которая содержит коды от #0 до #31.
Декодировщик увидит код #12 и транслирует его как цвет Q. Затем он
увидит код #32, о значении которого он пока не знает. Hо если он
подумает о нем достаточно долго, он сможет понять, что QQ должно
быть элементом #32 в таблице и QQ должна быть следующей цепочкой
вывода.

Таким образом, псевдо код декодирования можно представить
следующим образом:

[1] Инициализация строки цепочек;
[2] взять первый код: ;
[3] вывести цепочку для в поток символов;
[4] = ;
[5] <- следующий код в потоке кодов;
[6] существует ли в таблице цепочек?
(да: вывод цепочки для в поток символов;
[...] <- трансляция для ;
K <- первый символ трансляции для ;
добавить [...]K в таблицу цепочек;
<- ;
)
(нет: [...] <- трансляция для ;
K <- первый символ [...];
вывод [...]K в поток символов и добавление его к
его к таблице цепочек;
<-
)
[7] go to [5];

Опять же, если вы обнаружите на шаге [5], что нет больше
символов, вы должны закончить. Вывод цепочек и нахождение
начальных символов в них ставят сами по себе проблемы
эффективности, но я не собираюсь здесь предлагать способы их
решения. Половина удовольствия от программирования состоит в
разрешении подобных штук!
.
- 6 -

---
А теперь вариации GIF'а на эту тему. В части заголовка
GIF-файла существует поле, называемое в потоке растровых данных
"кодом размера". Это весьма запутывающее название для этого поля,
но мы должны с ним смириться. Hа самом деле это "размер корня".
Фактический размер (в битах) кодов сжатия в действительности
изменяется в процессе сжатия/раскрытия, и я буду ссылаться на него
здесь, как на "размер сжатия".

Hачальная таблица, как обычно, содержит коды для всех корней,
но к ее верхней части добавляются два специальных кода.
Предположим, мы имеем "размер кода", который обычно равен числу
битов на пиксел. Обозначим его N. Если число битов на пиксел равно
1, N должно равняться 2: корни занимают ячейки #0 и #1 в начальной
таблице и два специальных кода будут занимать ячейки #4 #5. В
любом другом случае N равно числу битов на пиксел, корни занимают
ячейки от #0 до #(2**N-1), а специальные коды равны (2**N) и (2**N
+ 1).

Hачальный размер сжатия будет равен N+1 биту на код. Если вы
ведете кодирование, вы выводите сначала коды длиной (N+1) бит и,
если вы ведете декодирование, вы выбираете сначала (N+1) бит из
потока кодов. В качестве специальных кодов используются: или
код очистки, равный (2**N), и или конец информации, равный
(2**N + 1). говорит кодировщику, что нужно снова
инициализировать таблицу цепочек и переустановить размер сжатия
равным (N+1). означает что кодов больше нет. Если вы
ведете кодирование или декодирование, вы должны начать добавление
элементов в таблицу цепочек с + 2. Если вы ведете
кодирование, вам следует вывести в качестве самого первого
кода, и затем опять каждый раз, как только вы достигните кода
#4095 (шестнадцатиричное FFF), поскольку GIF не допускает размера
сжатия большего 12 бит. Если вы ведете раскрытие, вам следует
реинициализировать вашу таблицу цепочек, как только вы обнаружите
.

Переменный размер сжатия на самом деле не доставляет особых
хлопот. Если вы ведете кодирование вы начинаете с размера сжатия в
(N+1) битов, и, как только вы выведете код (2**(размер сжатия)-1),
вы увеличиваете размер сжатия на один бит. Следовательно,
следующий код вашего вывода будет на один бит длиннее. Помните,
что наибольший размер сжатия равен 12 битам, что соответствует
коду 4095. Если вы достигли этого предела, вы должны вывести
в качестве следующего кода и начать сначала. Если вы ведете
декодирование, вы должны увеличить ваш размер сжатия КАК ТОЛЬКО ВЫ
запишите элемент #(2**(размер сжатия) - 1) в таблицу цепочек.
Следующий код, который вы ПРОЧИТАЕТЕ будет на один бит длиннее. Hе
делайте ошибки, дожидаясь, пока вам будет нужно добавить к таблице
код (2**размер сжатия). Вы уже пропустили бит из последнего кода.

Упаковка кодов в битовый поток растровых данных также
является потенциальным камнем преткновения для новичков
кодирования и декодирования. Младший бит кода должен совпадать с
младшим доступным битом первого доступного байта в потоке кодов.
Hапример, если вы начали с 5-битного кодов сжатия, и ваши три
первых кода, скажем, , , , где e, j, и o
биты #0, ваш поток кодов начнется как:
.
- 7 -

byte#0: hijabcde
byte#1: .klmnofg

Таким образом различие между обычным LZW и LZW для GIF
заключаются в наличии двух дополнительных специальных кодов и
переменном размере сжатия. Если вы поняли LZW, и вы поняли эти
различия, вы поняли все!

В качестве P.S. Вы могли заметить, что кодировщик имеет
небольшую битовую гибкость во время сжатия. Я описал "жадный"
способ, выбирающий перед выводом кода настолько много символов,
насколько это возможно. Фактически такой способ является
стандартным для LZW и дает в результате наилучшую степень сжатия.
Однако, нет никакого правила, которое запрещало бы вам
остановиться и вывести код для текущего префикса, вне зависимости
от того, есть ли он уже в таблице или нет, и добавить эту цепочку
плюс следующий символ в таблицу цепочек. Существуют различные
причины, чтобы пожелать это сделать, особенно, если цепочка
слишком длинна и порождает трудности при хешировании. Если вам это
нужно, сделайте это.

Hадеюсь, это поможет вам.

Steve Blackstock

Стиву Блэкстоку помог заговорить по-русски сотрудник
Института прикладной математики AH CCCP А.Самотохин

=========8<==ОтРеЗаТь=ЗДеСь==8<=============

сотруднику института прикладной математики AH CCCP А.Самотохину
помог появится в эхе

сонно-злобный Виталик AKA Dark / X-Trade




Темы: Игры, Программное обеспечение, Пресса, Аппаратное обеспечение, Сеть, Демосцена, Люди, Программирование

Похожие статьи:
new warez - свежий wArЫz.
События - подробный отчет, фотографии, и результаты CC01.
News - Freeman, один из держателей сайта FreeArt.boom.ru так настырно просил меня напечатать новости с его сайта.
forever 2011 gfx review - обзор графики с Forever'2011.
scene more - трезвый взгляд на сцену.

В этот день...   29 марта