Меню сайта |
|
 |
Статистика |
Онлайн всего: 1 Гостей: 1 Пользователей: 0 |
 |
|
 | |  |
| Главная » 2015 » Декабрь » 9 » Vinxru : Winapi аналог Getch()
17:41 Vinxru : Winapi аналог Getch() |
За прошлую неделю мне не один человек рассказал про MemSQL. Это новая база данных, которая работает исключительно в оперативной памяти. И со слов разработчиковмаркетологов это ноу-хау которое позволяет повысить производительность на несколько порядков. Маркетологи молодцы, все в курсе.
Да, здравый смысл в этом есть. Сейчас 16 Гб оперативной памяти может поставить любой, а этого объема хватит большинству баз данных. Причем, надо понимать, что файлы баз данных на диске занимают в 10-100 раз больше места, чем полезные данные. Если мы будем данные хранить в ОЗУ, то места потребуется меньше.
Почему? Диск позволяет "быстро"читать данные только блоками, притом расположенными подряд. И пытаясь выжать максимум скорости, объем приносится в жертву. И никто его не жалеет. А ОЗУ работает одинаково быстро во всем объеме. Такие жертвы не нужны и даже вредны. ОЗУ все таки мало.
А ненадежность хранения в ОЗУ решается тем, что данные в MemSQL так же записываются на диск :)
Это всё лишь маркетинг. То что они говорят о работе в памяти как о главном факторе - это маркетинг. Потому что в памяти работают многие БД. Из самых популярных: MySQL умеет создавать таблицы в ОЗУ, SQLite то же умеет всю базу держать в ОЗУ (:memory:). SQL-подобная технология LINQ в C# то же работает с данными в памяти.
Более того, печальный факт. Как только размер базы у обычной СУБД превышает размер ОЗУ, производительность начинает сильно падать. Так что уже давно оперативная память наращивается под размер БД.
Что же действительно позволяет обогнать конкурентов, так это грамотное программирование. Видимо парни пораскинули мозгами и написали более оптимальный код. И там есть куда оптимизировать. Многие вещи SQL-сервера делают очень медленно. Например вставку данных в таблицу. Разархивация БД (выполненная на основе INSERT INTO) размером в 50 Мб может длиться несколько часов.
Вот и я подумал, пора бы то же сделать свой маленький быстрый SQL с преферансом и дамами.
JIT
Делаю я его по своей инициативе по выходым, так как это потребует много времени. Неделю или две. А больше двух дней на задачу на работе мне не дают. Но даже если я не сделаю SQL, собственный JIT компилятор мне очень даже пригодится. До этого момента я писал все компиляторы в байт-код. Интерпретатор байт-кода можно написать на чистом Си, что позволяет компилировать его под любую архитектуру, избежать множества ошибок и легально работать с любым антивирусом. Антивирусы не очень хорошо относятся к модификации исполняемого кода. На идею байт-кода меня натолкнули исходные коды эмулятора процессора Z80 в далеком прошлом :) Я когда то писал свой эмулятор Спектрума, подглядывая в чужие эмуляторы. Причем, мой интерпретатор байт кода работал не медленнее PHP (язык программирования). И на этом я успокоился. Сейчас же нужна максимальная скорость, поэтому я перейду на уровень машинного кода. И первым моим экспериментом была простейшая программа
// Выделяем память из которой можно выполнять программы. Это основной момент
auto code = (unsigned char*)VirtualAlloc(0, 256, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE);
VirtualLock(code, 256);
// Записываем программу
code[0] = 0xC3; // код команды RET
// Выполняем
((void(*)())code)();
И она запустилась :) Далее начинаем писать грамотно, точнее пишем компилятор ассемблера. Это пока для 32-х битной версии, а для 64-х битной придется вносить небольшие изменения.
// Все условия перехода
enum Cc {
JA=7, JAE=3, JB=2, JBE=6, JC=2, JE=4, JG=0xF, JGE=0xD, JL=0xC, JLE=0xE, JNA=6,
JNAE=2, JNB=3, JNBE=7, JNC=3, JNE=5, JNG=0xE, JNGE=0xC, JNL=0xD, JNLE=0xF, JNO=1,
JNP=0xB, JNS=9, JNZ=5, JO=0, JP=0xA, JPE=0xA, JPO=0xB, JS=8, JZ=4
};
// Используемые регистры
enum Reg { EAX=0, ECX=1, EDX=2, EBX=3 };
// Команды ALU
enum Alu { ADD=0, OR=1, ADC=2, SBB=3, AND=4, SUB=5, XOR=6, CMP=7 };
class Compiler {
protected:
unsigned char *codeStart; // Начало кода
unsigned char *code; // Конец кода
unsigned char *prev; int cmd, cmdA, cmdB; // Для оптимизации
enum { CMD_mov_reg_imm=1, CMD_mov_reg_EBX_rel, CMD_mov_reg_reg, CMD_SET_A, CMD_set };// Для оптимизации
void s(char opcode) { cmd = 0; prev = code; *code++ = opcode; }
void s(char opcode1, char opcode2) { s(opcode1); *code++ = opcode2; }
void i(int imm) { *(int*)code = imm; code+=4; }
public:
Compiler() {
prev = code = codeStart = (unsigned char*)VirtualAlloc(0, 256, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE);
VirtualLock(code, 256);
}
void execute() {
((void(*)())codeStart)();
}
// Команды без оптимизации
void mov_reg_reg(Reg reg1, Reg reg2) { s(0x8B, 0xC0|(reg1<<3)|reg2); cmd=CMD_mov_reg_reg; cmdA=reg1; cmdB=reg2; }
void mov_reg_imm(Reg reg, int imm) { s(0xB8|reg); i(imm); cmd=CMD_mov_reg_imm; cmdA=reg; cmdB=imm; }
void mov_reg_EBX_rel(int reg, int rel) { s(0x8B, 0x83|(reg<<3)); i(rel); cmd=CMD_mov_reg_EBX_rel; cmdA=reg; cmdB=rel; }
void mov_EBX_rel_reg(int rel, Reg reg) { s(0x89, 0x83|(reg<<3)); i(rel); }
void push_reg(Reg reg) { s(0x50|reg); }
void pop_reg(Reg reg) { s(0x58|reg); }
void jcc(Cc cc, int label) { s(0x0F, 0x80|cc); fixup(code, label); i(-4); }
void jmp(int label) { s(0xE9); fixup(code, label); i(-4); }
void ret() { s(0xC3); }
void set(Cc cc, Reg reg) { auto p=code; mov_reg_imm(reg,0); s(0x0F, 0x90|cc); *code++ = 0xC0|reg; prev=p; cmd=CMD_set; cmdA=cc; }
// Команды с оптимизацией
void needFlags_jcc(Cc cc, int label) {
if(cc==JZ && cmd==CMD_set) { code = prev; jcc((Cc)cmdA, label); return; }
alu_reg_reg(OR, EAX, EAX);
jcc(cc, label);
}
void alu_reg_imm(Alu alu, Reg reg, int imm) {
if(reg==EAX) { /* alu eax, imm */ s(0x05+(alu<<3)); i(imm); return; }
/* alu reg1, imm */ s(0x81, 0xC0+(alu<<3)+reg); i(imm);
}
void alu_reg_reg(Alu alu, Reg reg1, Reg reg2) {
/* alu reg1, imm */ if(cmd==CMD_mov_reg_imm && cmdA==reg2 ) { code=prev; alu_reg_imm(alu, reg1, cmdB); return; }
/* alu reg1, [ebx+rel] */if(cmd==CMD_mov_reg_EBX_rel && cmdA==reg2 ) { code=prev; s(0x03+(alu<<3), 0x83+(reg1<<3)); i(cmdB); return; }
s(0x03|(alu<<3)); *code++ = 0xC0|(reg1<<3)|reg2;
}
void idiv_reg(Reg reg) {
/* idiv [ebx+rel] */ if(cmd == CMD_mov_reg_EBX_rel) { code = prev; s(0xF7, 0xBB); i(cmdA); return; }
/* idiv reg */ s(0xF7, 0xF8|reg);
}
void imul_reg(Reg reg) {
/* imul [ebx+rel] */ if(cmd==CMD_mov_reg_EBX_rel && cmdA==reg) { code = prev; s(0xF7); *code++ = 0xAB; i(cmdB); return; }
/* imul reg */ s(0xF7); *code++ = 0xE8|reg;
}
// Метки
struct Fixup {
unsigned char* code;
int label;
Fixup() {};
Fixup(unsigned char* _code, int _label) { code=_code; label=_label; }
};
std::vector<Fixup> fixups;
std::vector<unsigned char*> labels;
void fixup(unsigned char* code, int label) { fixups.push_back(Fixup(code, label)); }
void label(int label) { labels[label] = code; }
int allocLabel() { labels.push_back(code); return labels.size()-1; }
void prepare() { for(auto f=fixups.begin(), fe=fixups.end(); f!=fe; f++) *(int*)f->code += labels[f->label] - f->code; }
};
Ради облегчения себе жизни в будущем я буду использовать самые простые варианты инструкций. Например команду умножения работающую только с регистрами. А уже компилятор ассемблера (код выше) будет объединять пару простых инструкций в более сложные. Например: MOV EDX, [EBX+?] + MUL EDX => MUL [EBX+?]. И во вторых, этот объект занимается компиляцией меток и переходов. Во время формирования кода, команд перехода, мы не знаем адрес перехода, если метка расположена дальше по коду. Для этого в команде перехода мы вызываем метод fixup(allocLabel()), он запоминает меcто куда надо будет записать адрес. А когда настает очередь метки, мы вызываем label, она запоминает адрес метки. А в конце вызывается метод prepare. Который расставляет все адреса. Ну и ради теста пишем программу:
struct Item {
int a, b;
};
std::vector<Item> data;
#define OFFSET(X) ((char*)&Item.X-(char*)&Item.a)
void do() {
Compiler c;
c.mov_reg_imm(EBX, (int)data.begin()._Ptr);
int l0 = c.allocLabel();
c.mov_reg_EBX_rel(EAX, OFFSET(a));
c.alu_reg_imm(CMP, EAX, 16);
int l = c.allocLabel();
c.jcc(JA, l);
c.mov_EBX_rel_imm(OFFSET(b), 1);
c.label(l);
c.alu_reg_imm(ADD, EBX, sizeof(item));
c.alu_reg_imm(CMP, EBX, (int)data.end()._Ptr);
c.jcc(JNZ, l0);
c.ret();
c.prepare();
c.execute();
}
Эта программа выполняет действие "if(a > 16) b = 1". Выполняется быстро, но пока это совсем не похоже на SQL. Теперь нужен компилятор c SQL. Пока что компилятор простейшего выражения. Причем, компилятор мог быть маленьким. Но я ради оптимизации решил разделить его на две части. На разбор кода и компиляцию кода собственно. Заброшенный компилятор Си для 8080 был написан одним куском, что насколько усложнило код, что я год к нему не хотел возращаться. С новой архитектурой всё проще и опять захотелось :)
enum Type { tAcc, tVar, tImm, tStack };
struct Var {
Type type;
int n;
void set(Type _type, int _n) { type=_type; n=_n; }
~Var();
};
class Node {
public:
Var v; // Переменная или константа
Operator o; // Оператор
Cc cc; // Условие перехода
Node *a, *b; // Аргументы оператора. Надо заменить на list args
Node(Var& _v) { o=oNone; a=0; b=0; v=_v; }
Node(int& n) { o=oNone; a=0; b=0; v.n=n; v.type=tImm; }
Node(Operator _o, Cc _cc, Node* _a, Node* _b) { o=_o; cc=_cc; a=_a; b=_b; }
};
Node* parseVar(int level=0) {
Node* a;
if(p.ifToken("(")) { a = parseVar(0); p.needToken(")"); } else
if(p.ifToken(ttInteger)) { a = new Node(p.i); } else
if(p.ifToken(varNames)) { a = new Node(vars[p.i]); } else p.syntaxError();
while(true) {
Operator o; int l; Cc cc=JZ;
if(level<1 && p.ifToken("Or")) l=1, o=oOr; else
if(level<2 && p.ifToken("And")) l=2, o=oAnd; else
if(level<3 && p.ifToken("<" )) l=3, o=oCmp, cc=JB; else
if(level<3 && p.ifToken("<=")) l=3, o=oCmp, cc=JBE; else
if(level<3 && p.ifToken(">" )) l=3, o=oCmp, cc=JA; else
if(level<3 && p.ifToken(">=")) l=3, o=oCmp, cc=JAE; else
if(level<3 && p.ifToken("==")) l=3, o=oCmp, cc=JE; else
if(level<3 && p.ifToken("!=")) l=3, o=oCmp, cc=JNE; else
if(level<4 && p.ifToken("+" )) l=4, o=oAdd; else
if(level<4 && p.ifToken("-" )) l=4, o=oSub; else
if(p.ifToken("*")) l=5, o=oMul; else
if(p.ifToken("/")) l=5, o=oDiv; else return a;
Node* b = parseVar(l);
a = new Node(o, cc, a, b);
}
}
Эта функция строит дерево выражения. Некий аналог Expression Trees в .NET или еще его можно представить как программу на LISP. (это Abstract syntax tree (с) ex0_panet) Помимо разделения кода на две части, это древеро очень удобно оптимизировать. Но этот момент я пока опущу. Компилятор получился не очень большим:
void compileVar(Var* x, Node* n, int jmpIfFalse=-1) {
// Это переменная или константа
if(n->o==oNone) {
*x = n->v;
return;
}
// AND и OR обрабатываем по особому.
if(n->o==oAnd || n->o==oOr) {
if(x) jmpIfFalse = c.allocLabel();
if(n->o==oOr) {
int jmpIfTrue = c.allocLabel();
int elseLabel = c.allocLabel();
compileVar(0, n->a, elseLabel);
c.jmp(jmpIfTrue);
c.label(elseLabel);
compileVar(0, n->b, jmpIfFalse);
c.label(jmpIfTrue);
} else {
compileVar(0, n->a, jmpIfFalse);
compileVar(0, n->b, jmpIfFalse);
}
if(x) {
c.mov_reg_imm(EAX, 1);
int l = c.allocLabel();
c.jmp(l);
c.label(jmpIfFalse);
c.mov_reg_imm(EAX, 0);
c.label(l);
accUsed = x;
x->type = tAcc;
}
return;
}
// Получаем аргументы оператора
Var a; compileVar(&a, n->a);
Var b; compileVar(&b, n->b);
// Помещаем арументы в EAX, EDX
if(b.type==tAcc) push(EDX, b);
push(EAX, a);
if(b.type!=tAcc) push(EDX, b);
// Выполняем оператор
switch(n->o) {
case oCmp: c.alu_reg_reg(CMP, EAX, EDX); /* Требуется значение */ if(x) c.set(n->cc, EAX); break;
case oAdd: c.alu_reg_reg(ADD, EAX, EDX); break;
case oSub: c.alu_reg_reg(SUB, EAX, EDX); break;
case oDiv: c.idiv_reg(EDX); break;
case oMul: c.imul_reg(EDX); break;
default: FatalAppExitA(1, ":)");
}
if(x) {
// Требуется значение
accUsed = x;
x->type = tAcc;
} else {
// Требуется условие
c.jcc(n->cc, jmpIfFalse);
}
}
// Поместить значение в регистр
void push(Reg reg, Var& x, bool freeAcc=false) {
switch(x.type) {
case tVar: c.mov_reg_EBX_rel(reg, x.n); break;
case tImm: c.mov_reg_imm(reg, x.n); break;
case tAcc: if(reg!=EAX) c.mov_reg_reg(reg, EAX); break;
case tStack: c.pop_reg(reg); break;
}
x.type = tAcc;
if(freeAcc) accUsed=0;
}
И его проверка. Здесь мы компилируем выражение (a > b OR a > 18) AND b > c и его результат записываем в Item::result.
// Начало цикла
// for(EBX=data.begin(); EBX!=data.end(); EBX++) {
c.mov_reg_imm(EBX, (int)data.begin()._Ptr);
int l0 = c.allocLabel();
// Компиляция выражения
p.load("(a > b OR a > 18) AND b > c");
Var out;
compileVar(&out, parseVar());
p.needToken(ttEof);
// Сохраняем выражение в EBX->resut
push(EAX, out, true);
c.mov_EBX_rel_reg(OFFSET(result), EAX);
// Конец цикла for(EBX=data.begin(); EBX!=data.end(); EBX++)
c.alu_reg_imm(ADD, EBX, sizeof(item));
c.alu_reg_imm(CMP, EBX, (int)data.end()._Ptr);
c.jcc(JNZ, l0);
c.ret();
// Запуск
c.prepare();
c.execute();
Добавить несколько простых строк и этот код уже сможет выполнять запрос типа: SELECT выражение, выражение, выражение FROM таблица WHERE выражение. Но надо еще сделать поддержку типов данных std::string, double, функций. Это не сложно. Чуть сложнее будет с компиялцией слов GROUP BY, HAVING. Но я это уже делал, только транслировал не в машинный код в программу на Паскале. SIMD
Что действительно заставляет задуматься, дак это применение SIMD инструкций (MMX/SSE/AVX/AVX-512). Это инструкции выполняющие одинаковую операцию для несколькими числами одновременно (за один такт). SSE инструкция обрабатывает за такт 128 бит данных, то есть 4 целых 32-х битных числа или 2 дробных 64-битных. Более поздние наборы инструкций AVX или AVX-512 позволяют обрабатывать 256 или 512 бит за раз. В SQL-запросах одинаковые выражения применяются к миллионам разных строк, поэтому эти инструкции напрашиваются сами собой. Но для них необходимо данные распологать в определенном порядке. Сначала идут значения одного слобика таблицы, затем второго, затем третьего. Это надо, что бы процессор смог одним запросом вытащить из памяти значения сразу нескольких ячеек. Как то мне один весьма умный человек, преподаватель по программирования в университете, сказал: SIMD тебе не нужен, даже обычные инструкции обрабатывают данные быстрее, чем работает ОЗУ. Прироста скорости не будет... В прошлом я написал несколько компиляторов Пролога. И очень хотелось написать транслятор Пролога на Си, но это было невозможно, потому что в Прологе программа выполняется весьма необычным способом. Вызов функции может привести к многократному вызову функций по коду ниже. (Правильнее говорить предикат, а не функция). Например, в самом простом случае.
Совершеннолетние_клиенты(Имя) :-
Клиенты(Имя, Возраст),
Возраст > 18;
Первая строка выполнит многократный вызов второй строки. (Это зависит от компилятора, но предположим самый глупый компилятор). И на Си написать аналог этого примера было сложно. Конечно возможно, но машинный код был понятнее. Вообще, в Прологе порядок строк может быть любой и на результат это не влияет. Каждая строка порождает множество вариантов и все они объединяются по И. Программа выше звучит как "Все что является клиентом"и "Всё что больше 18 лет". Лямбда функции, появившиеся недавно в C++, позволяют красиво, то есть без хаков, представить на С++ программу на Прологе. Возьмем пример из Википеди (мне лень искать что то более интересное). Программа поиска общего делителя для массива чисел:
% Верно, что НОД (A, 0) = A
gcd2(A, 0, A).
% Верно, что НОД(A, B) = G, когда A>0, B>0 и НОД(B, A % B) = G (% - остаток от деления)
gcd2(A, B, G) :- A>0, B>0, N is mod(A, B), gcd2(B, N, G).
gcdn(A, [], A).
gcdn(A, [B|Bs], G) :- gcd2(A, B, N), gcdn(N, Bs, G).
gcdn([A|As], G) :- gcdn(A, As, G).
На C++ с Лямбдами это будет выглядеть так:
void gcd2(int a, int b, std::function<void(int)> result) {
// Верно, что НОД (A, 0) = A
if(b==0) result(a);
//Верно, что НОД(A, B) = G, когда A>0, B>0 и НОД(B, A % B) = G (% - остаток от деления)
if(a>0 && b>0) gcd2(b, a % b, result);
}
void gcdn(int a, int* bb, int* be, std::function<void(int)> result) {
if(bb==be) result(a);
else gcd2(a, *bb, [&](int n) { gcdn(n, bb+1, be, result); });
}
void gcdn(int* xb, int* xe, std::function<void(int)> result) {
gcdn(*xb, xb+1, xe, result);
}
void test() {
vector<int> numbers;
numbers.push_back(36);
numbers.push_back(6);
numbers.push_back(10);
gcdn(numbers.begin(), numbers.end(), [&](int n) {
cout << n;
});
}
В принципе, можно компилировать с Пролога на Си красиво
А почему бы сразу не писать так? Компилятор пролога строит план выполнения, переставляет строки, объединяет предикаты, выбрасывает лишнее. А еще определяет, какая переменная будет входящей, а какая исходящей. В предикате A=B*C, входящей может быть любая переменная и даже все сразу. Вот за этим он и нужен.
А пригодится всё это для построения оптимизаторов.
ОБЖ мне нравилось. На нём мы проводили репетицию атомной войны. Нам рассказывали про атомные взрывы, что во время взрыва лучше всего лечь в ванную. Она защитит от проникающей радиации и от упавшего перекрытия. И рассказывали как выжить в постапокалиптическом мире :) Про время распада радиоактивного йода. Рассказывали, какие таблетки надо поедать. Каждые пол года в городе объявлялась учебная тревога и мы ровными рядами бежали из школы до ближайшего бомбоубежища. Тревога реально внушала страх - вой сирен по всему городу.
Помимо атомной угрозы, в городе были еще больше запасы хлора и аммиака. В случае аварии, облака ядовитого газа закроют город. От хлора надо бежать на крыши, а от аммиака наоборот лечь на пол. И нас учили как в домашних условиях изготовить простейшую защиту органов дыхания.
И это мне пригодилось! Однажды дома я открыл банку с раствором хлора (или жидким хлором), мне стало плохо и я тут же выбежал из комнаты. У меня чуть глаза и лёгкие не "лопнули". И вот комната закрыта, я стою снаружи и думаю, а как оттуда банку забрать?
Я взял тазик, налил воды, растворил соду. В этом растворе вымочил полотенце. На голову одел прозрачный пакет, так что бы он закрывал только глаза. А рот (нижнюю часть головы) обмотал этим пропитанным содой полотенцем. И вполне комфортно пошел в комнату, открыл окна и закрыл банку.
Но ОБЖ-шник был типичным воякой. И всё у нас было как в армии, даже дедовщина :)
У нас в классе самыми важными были второ- и тертьегодники. Во первых они были испорченными, а это привлекало. Например, они закидывали мощные петарды, коробки с казеином или газеты пропитанные селитрой в окна переполненных отъезжающих автобусов. Во вторых, они всем рассказывали ощущения от приёма наркотиков и где их купить. В частности ощущения от Тарена, украденного из аптечек АИ-2, которые нам показывали на ОБЖ. Вокруг них собирались целы толпы учеников. А в третьих, их крышевали старшие братья, поэтому их даже коллективно бить было страшно. Гопники гороче.
И вот в один из обычных школьных дней, прямо в коридоре учебного заведения эти гопники начали пинать одного из одноклассников. Заставляли его маршировать и пинали под зад. А если он отказывался, то били уже кулаками.
По коридору идёт ОБЖ-шник и спрашивает: - Что тут происходит?
Я думаю, ура, их накажут! Но ОБЖ-шик то же присоединился к ним и стал объяснять:
- Мы учим его маршировать! - Молодцы! Что ты двигаешься как Буратино! Ноги выше!
И пнул его. Ну и вы понимаете, какая кличка была у человека после этого. Я тогда офигел. ОБЖ-шник был своим мужиком для гопников, он никогда не стучал завучу на класс. Даже когда один из блатных учеников прямо на уроке сказал:
- Ты говнюк! И что бы мне сделаешь? - Да как ты со мной разговариваешь, я капитан второго ранга! - Сифон у тебя второго ранга. Что ты сделаешь старая гнида?
ОБЖ-шник отвернул глаза и продолжил урок.
В преддверии термоядерной войны я решил почитать, что нового изобрели ученые и политики. Ведь атомные бомбы изобрели в 1950-х годах. Пора изобретать новое оружие. Представьте технику 1960-х годов! И атомные бомбы прямиком оттуда. Нам нужны новые бомбы 21-ого века. Они будут еще мощнее, еще ядовитее, еще разрушительнее! При этом будут обладать интеллектом и никакая ПРО их не остановит. И это будущее уже не за горами:
Во первых, договор СНВ-III был подписан Россией и США в Праге в 2010г можно считать разорванным. В течении 10 лет стороны должны сократить ядерные боезаряды до 1550 единиц, а межконтинентальные баллистические ракеты подводных лодок и тяжелых бомбардировщиков - до 700 единиц. Поскольку США разорвало отношения между военными ведомствами США и России, нормальных регулярных двухсторонних контактов по соблюдению договоров быть не может и договор можно считать разорванным.
Во вторых, Россия и США давно уже проводят ядерные испытания. Только теперь они называются "неядерными ядерными испытаниями"или "подкритическими ядерными взрывами". Суть в том, что бомба содержит недостаточное для цепной реакции количество радиоактивного материала. После нажатия на красную кнопку, все так же происходит химический взрыв приводящий к обжатию плутония. Так же ускоряется расщепление плутония, но ускоряется недостаточно для моментального взрыва.
Ученые из такой модели могут получить гораздо больше полезных сведений, чем от полноценного взрыва. После взрыва от бомбы ничего не остается. А в случае подкритичного ядерного испытания, обжатый плутоний можно исследовать и использовать в следующей бомбе. Можно однозначно спрогнозировать силу ядерного взрыва.
Но и мирный атом не стоит на месте. Китай продает ядерные светящиеся брелки, их уже все видели. А вот ядерные батарейки, я увидел сегодня впервые. Такую бы в смартфон!
![H-DOLBAQ1zE[1]](http://ic.pics.livejournal.com/vinxru/24784160/258876/258876_1000.jpg)
http://item.taobao.com/item.htm?spm=a1z10.1.4004-56656384.29.dysKyM&id=15659486189
Говорят, что батарейка дает 3 Вольта, 50.0 мкВт / 3 = 16 мкА в течении 20 лет. Для такой цены это мало. Но все когда-то начиналось с заоблачных цен и микроскопического кпд.
Переделал все на новый стандарт. Осталось командер отладить (глючит). Ну и потом добавить эмулятор магнитофона.
Работает на удивление шустро. Скорость можно глянуть тут:
UPD: Надо отключать автоматическую регулировку яркости при съёмке монитора :)
Сначала запускается SD STARTER, программа размером меньше 100 байт. Она всего лишь запускает файл SD BIOS (boot/sdbios.rks).
Идея в том, что SD STARTER потом будет загружаться через порт магнитофона (эмулятором магнитофона спрятанном внутри контроллера). И хранится на флешке в файле boot/boot.rks. Благодаря этому не требуется изменение стандартного ПЗУ компьютера. То есть не нужно дорабатывать компьютер. Скорость загрузки с магнитофона не очень большая, всего 150 байт/сек, поэтому эту программу я сделал максимально маленькой.
Но лично я засунул SD STARTER в ПЗУ. Так компьютер запускается быстрее и порт магнитофона остается свободным. Самое большое неудобство программ в ПЗУ, это невозможность исправлять ошибки. И тут маленький объем стартера (и минимум функций) как нельзя кстати. Меньше вероятность наделать ошибок. Новые функции в нем никогда не появятся. И я надеюсь, что мне его никогда не придется переделывать.
Что не скажешь о SD BIOS-е. Это набор подпрограмм для работы с контроллером.
Стартер запускает БИОС. БИОС получает версию контроллера, выводит её на экран. То есть на экране видно три строки:
SD STARTER 1.0 SD BIOS 1.0 SD CONTROLLER 1.0
БИОС состоит из двух частей. Резидентной, которая всегда должна находится в памяти. Это собственно подпрограммы для работы с контроллером. И часть, выполняющая запуск компьютера, которая потом уничтожается. Эта часть рисует на экране изображение флешки, что то пишет и запускает файл boot/shell.rks, который является командором.
Вместо любого файла (boot, sdbios, shell) можно подсунуть любую игру/программу. Она будет работать, только БИОСА в памяти не будет. А он может быть и не нужен.
После запуска БИОСА карта памяти выглядит так:
С самого начала памяти свободно 35 537 байт памяти (до 8AD1h). Дальше находятся переменные SELF_NAME, CMD_LINE. Если они не нужны программисту, то объем непрерывной свободной памяти составляет ~36 049 байта (до 8CD1h).
8AD1h SELF_NAME Собственное имя файла 256 байт (этот адрес содержится в DE после запуска)
8BD1h CMD_LINE Командная строка 256 байт (этот адрес содержится в HL после запуска)
8CD1h SD_BIOS_CODE Код SD BIOS 559 байт (возможно будет расти в начало сдвигая SELF_NAME и CMD_LINE)
8F00h - Не используется ~233 байта (возможно тут есть переменные монитора)
8FDFh MONITOR_VARS Переменные монитора
9000h VIDEO_MEM Видеопамять
Свободное место по адресу 8F00h оставлено на всякий случай. Во первых, что бы не конфликтовать с Монитором. Возможно, что будущие версии SD BIOS будут использовать эту память.
SD BIOS может быть в любом месте памяти. При запуске программы регистр A содержит версию набора команд (сейчас 1). BС содержит точку входа. HL командную строку. DE собственное имя.
Для вызова функции контроллера, надо поместить в регистр A код функции и вызвать подпрограмму по адресу переданному при старте программы в регистре BC.
Функции контроллера:
Reboot Теплая перезагрузка (A=0, HL="", DE="" / A=код ошибки)
Exec Запустить программу (A=0, HL=имя файла, DE=командная строка / A=код ошибки)
FindFirst Начать получение списка файлов (A=1, HL=путь, DE=максимум файлов для загрузки,
BC=адрес / HL=сколько загрузили, A=код ошибки)
FindNext Продолжить получение списка файлов (A=1, HL=":", DE=максимум файлов для загрузки,
BC=адрес / HL=сколько загрузили, A=код ошибки)
Open Открыть файл (A=2, D=0, HL=имя файла / A=код ошибки)
Create Создать файл (A=2, D=1, HL=имя файла / A=код ошибки)
MkDir Создать папку (A=2, D=2, HL=имя файла / A=код ошибки)
Delete Удалить файл/папку (A=2, D=100, HL=имя файла / A=код ошибки)
Seek Установить позицию чтения записи файла (A=3, B=режим, DE:HL=позиция / A=код ошибки, DE:HL=позиция)
(С начала B=0, с текущего положения B=1, с конца B=2)
GetSize Получить размер файла (A=3, B=100 / A=код ошибки, DE:HL= размер файла)
Swap Работа с двумя открытыми файлами (A=3, B=200 / A=код ошибки)
Read Прочитать из файла (A=4, HL=размер, DE=адрес / A=код ошибки, HL=сколько загрузили)
Write Записать в файл (A=5, HL=размер, DE=адрес / A=код ошибки)
WriteEOF Конец файла (A=5, HL=0 / A=код ошибки)
Move Переместить файл/папку (A=6, HL=из, DE=в / A=код ошибки)
GetFree Размер флешки и свободное место на флешке (A=7, HL=буфер / A=код ошибки)
Давно, давно, давно надо было сделать этот контроллер. Но в начале этого года отвлек меня SysCat, подарив плату Специалиста. :) И все же возвращаюсь. Первая версия платы выглядит так, схема в самом низу.

Все просто. Контроллер подключается к параллельному порту. Контакты расположены так, что бы подключаться к Апогею БК01. А внутри МК находится эмулятор ПЗУ и запустить программу оттуда можно с помощью стандартной директивы R0,100 G. Загруженная программа уже будет работать с драйвером файловой системы FAT32, которые так же находится внутри МК.
Протокол будет таким:
Сначала на шину адреса устанавливаем значения 19, 23, 87. Они отличаются одним битом, поэтому шума между ними быть не должно.
Далее, компьютер читает адреса 86,87,86,87,86,87... каждый байт 86 является тактовым импульсом, т.е. подтверждением приема/передачи байта.
Если синхронизация удалась, то МК выставляет на шину данных ERR_START, а после тактового импульса ERR_OK_WAIT. Код ERR_OK_WAIT может держаться неопределенное кол-во тактов. В это время происходит инициализация флешки.
Потом ERR_OK_WAIT меняется на ERR_OK_NEXT если все нормально. Или возвращается код ошибки.
Компьютер должен переключить порт на выход и передать команду. В примере ниже команда BOOT - единственный байт 0
На это МК отвечает ERR_OK_WAIT (много раз), ERR_OK_ADDR, адрес загрузки L, адрес загрузки H, ERR_OK_WAIT (много раз)
Потом выдает содержимое файла блоками: 0, длина L, длина H, данные (HL раз), ERR_OK_WAIT (много раз)
Если данных не осталось, выдает ERR_OK_READ или код ошибки
Любое значение на шине адреса кроме 86, 87 вызовет отключение SD контроллера и включение эмулятора ПЗУ. Поэтому начальный загрузчик всегда можно загрузить директивой R0,100
Код:
.org 0h
DATA_PORT = 0EE00h
ADDR_PORT = 0EE01h
CTL_PORT = 0EE03h
SEND_MODE = 10001011b ; Настройка: 1 0 0 A СH 0 B CL 1=ввод 0=вывод
RECV_MODE = 10011011b
ERR_START = 040h
ERR_WAIT = 041h
ERR_OK_DISK = 042h
ERR_OK = 043h
ERR_OK_READ = 044h
ERR_OK_ENTRY = 045h
ERR_OK_WRITE = 046h
ERR_OK_ADDR = 047h
Entry:
; Первым этапом происходит синхронизация с контроллером
; 256 попыток. Для этого в регистр C заносится 0
; А в стек заносится адрес перезагрузки 0C000h
LXI B, 0F800h
PUSH B
JMP Boot
NOP
;----------------------------------------------------------------------------
; Отправка и прием байта
Rst1:
INX H ; HL = ADDR_PORT
MVI M, 86
MVI M, 87
DCX H ; HL = DATA_PORT
MOV A, M
RET
;----------------------------------------------------------------------------
; Ожидание готовности МК
Rst2:
WaitForReady:
Rst 1
CPI ERR_WAIT
JZ WaitForReady
RET
;----------------------------------------------------------------------------
RetrySync:
; Попытки
DCR C
RZ ; Ошибка синхронизации, перезагрузка
Boot:
; Режим передачи (освобождаем шину) и инициализируем HL
MVI A, RECV_MODE
CALL SetMode
; Начало любой команды
MVI M, 19
MVI M, 23
MVI M, 87
; Если есть синхронизация, то контроллер ответит ERR_START
Rst 1
CPI ERR_START
JNZ RetrySync
; Дальше будет ERR_OK_WAIT, ERR_OK_NEXT
; Инициализация флешки
Rst 2
CPI ERR_OK_ADDR
JNZ Rst1 ; Ошибка, освобождаем шину и перезагрузка
; ERR_OK_NEXT высталенный МК будет висеть на шине до следующего RST,
; только после него МК освободит шину и мы сможем включить режим передачи.
Rst 1
; Режим передачи
MVI A, SEND_MODE
CALL SetMode
; Код команды
XRA A
Rst 1
; МК читает данные во время тактового импульса, т.е. он уже их прочитал.
; Включаем режим приема, т.е. освобождаем шину.
; Режим приема
MVI A, RECV_MODE
CALL SetMode
; МК захватит шину во время тактового импульса (первого RST)
; Дальше будет ERR_OK_WAIT, ERR_OK_RKS
Rst 2
CPI ERR_OK_RKS
JNZ Rst1 ; Ошибка, освобождаем шину и перезагрузка
; Удаляем из стека 0F800h
POP B
; Адрес загрузки в BC
Rst 1
MOV C, A
Rst 1
MOV B, A
; Сохраняем в стек адрес запуска
PUSH B
; Подождать, пока МК прочитает очередной блок денных
RecvLoop:
Rst 2
CPI ERR_OK_READ
JZ Rst1 ; Всё загружено, освобождаем шину и запуск
ORA A
JNZ 0F800h ; Ошибка, перезагрузка (не отпускаем контроллер)
; Принять очередной блок
Rst 1
MOV E, A
Rst 1
MOV D, A
RecvBlock:
Rst 1
STAX B
INX B
DCX D
MOV A, E
ORA D
JNZ RecvBlock
JMP RecvLoop
; Прием/передача
SetMode:
LXI H, CTL_PORT
MOV M, A
DCX H
DCX H ; HL = ADDR_PORT
RET
.End
108 байт.
Схема
 (RESET забыл :)
https://github.com/vinxru/MXOS
Исправленные недоработки.
В файле DOS.SYS ● BIG_MEM - Поддержка ДОЗУ большего объема, чем 64 Кб ● ROM_64K - Размер ПЗУ у Специалиста MX2 всего 64 Кб, но без этой опции будет работать лишь 32 Кб ● LOAD_FONT - Загружать шрифт в ОЗУ (ускорение работы и возможность загрузки ОС с любого накопителя) ● DISABLE_COLOR_BUG - Включить инициализацию контроллера цвета при запуске
В файле NC.COM ● ENABLE_COLOR - Включить цвет ● BIG_MEM - Поддержка ДОЗУ большего объема, чем 64 Кб ● DISABLE_FREE_SPACE_BUG - Исправить ошибку определения свободного объема
О чем я вообще?
MXOS - это название операционной системе я дал сам, поскольку оригинального названия я не нашел. Народ её называл командер, а внутри оперционки содержатся лишь строки: BIOS 4.40, COMMANDER VERSION 1.4, (C) OMSK 1992.
MXOS это альтернативная операционная система для компьютера Специалист МХ. Оригинальна операционная система называлась RamFOS.
В отличии от RamFOS эта операционная система написана более грамотно. Для доступа к накопителям используются драйвера. Чтение и запись происходит 256 байтными блоками. В RamFOS доступ был побайтный и при том частый и хаотичный, что не позволяло подключить дисковд или SD карту в качестве накопителя.
Так же MXOS более шустрая и обладает более приятным интерфейсом напоминающим Norton Commander. Любое расширение файла можно привязать к любой программе. Список соответствий хранится в файле NC.EXT, максимальный размер которого 36 Кб.
Но при этом MXOS содержит меньше сервисных возможностей и не совместима с RamFOS и стандартным Специалистом. Основные отличия, это:
● Нет поддерживаемых системой верхней и нижней строки состояния ● Система не содержит форму для открытия файла ● Нет поддержки звука на основе ВИ53 ● Нет поддержки цвета (хотя и RamFOS его полноценно не поддерживал и не использовал вообще) ● Поддерживается значительно меньше специальных кодов при выводе на экран ● Поддерживается лишь одна модель принтера ● Оригинальная ОС поддерживает лишь 64 Кб дополнительного ОЗУ (что в сумме дает 128 Кб), которое используется как RAM-диск. Запуск с большим объемом памяти приведет к зависанию, так как в порт выподра страницы записывается случаное число. Исправленная версия ОС поддерживает 448 Кб дополнительной памяти, т.е. 7 страниц как накопители B-H.
Многие возможности можно реализовать дополнительными модулями. Например, в комплекте идет драйвер ПЗУ подключаемого к порту расширения. Таким же образом могла быть выполнена поддержка дисковода, но драйвера у меня нет.
Поддерживаются две кодировки KOI-7 и KOI-8, переключаемые как с клавиатуры, так и ESC-последовательностями (ESC+'('и ESC+')'). Знакогенератор содержит 256 символов.
Используется раскладка клавиатуры Стандартного специалиста. У MX отличаются коды клавиш F1-F10, TAB, ESC.
MXOS поддерживает BAT-файлы и передачу аргументов запускаемым программам. При холодной перезагрузке запускается файл B:AUTOEX.BAT, затем A:FORMAT.COM B:, а затем A:NC.COM. Устройство A: - это ПЗУ, устройство B: - это оперативная память. Максимально поддерживается 8 устройств.
При создании собственного ПЗУ (загрузочного диска A:) вы можете разместить AUTOEX.BAT так же и на диске A:, а в нем разместить запуск драйверов.
Запуск MXOS

Если при запуске компьютера зажать клавишу ?, то MXOS сразу перейдет к загрузке программы с магнитофона. Если при запуске компьютера зажать клавишу ?, то MXOS пропустит запуск B:AUTOEX.BAT.
При перезагрузке инициируемой программами запускается лишь A:NC.COM.

Имя файла состоит из 6+3 символов. Ввод расширения при запуске файла из ком. строки обязателен. Папки самой операционной системой не поддерживаются, но это можно реализовать через драйверы. Максимальное кол-во файлов в папке - 48 шт, но оболочка поддерживает отображение лишь 36 файлов. Остальные файлы вы не увидите, не сможете выполнять над ними действия из оболочки, но сможете запустить их ком строки. Максимальный размер файла - 64 Кб, но оболочка может работать лишь с ~36 Кб. При попытке скопировать (и т.п.) файл большего размера произойдет переполнение буфера, уничтожение системных переменных, затем экрана, затем самой ОС в зависимости от размера файла. Структура хранения файлов напоминает FAT, только используются 8 битные номера кластеров.
В отличии от "монитора"стандартного Специалиста, MXOS не содержит режима работы с консоли и соответственно директив вводимых с клавиатуры. Вся работа происходит в диалоговом режиме.
Оболочка поддерживает две панели, как Norton Commander.

Клавиши выполняют следующие команды:
● ESC - Очистка ком строки ● TAB - Переход между панелями ● F1, F2 - Выбор накопителя для левой и правой панели ● F3 - Отображение на неактивной панели информации о накопителе активной панели (как на фото выше) ● F4 - Запуск внешнего редактора E.COM для выбранного файла ● F5 - Копирование файла ● F6 - Изменение имени файла / перемещение ● F7 - Загрузка файлов с ленты (магнитофона) на накопитель ● F8 - Удаление файла ● F9 - Сохранение файла с накопителя на ленту
Есть и другие клавиши, но я пока не разбирался
Раскрасил командер
Добавил ключ ENABLE_COLOR, который включает эту раскраску.

Ядро (DOS.SYS)
Представляет собой набор подпрограмм по адресу C800h. Это подпрограммы обслуживания экрана, принтера, клавиатуры и накопителя на магнитой ленте и файловой системы.
Зеленым отмечены функции соврапающие со стандратным Специалистом. Но галвная проблема невосместимости - Аппаратная. Адрес порта клавиатуры у MX изменен.
C800h
reboot
Запуск файла A:NC.COM
C803h
getch
Ввод символа с клавиатуры; A-код
C806h
tapeRead
Ввод байта с ленты; A-код; если ошибка, то происходит переход на адрес по адресу 8FE1h
C809h
printChar
Вывод байта на экран; C-код
C80Сh
tapeWrite
Вывод байта на ленту; C-байт
C80Fh
input
Ввод строки с клавиатуры
C812h
keyScan
Ввод кода нажатой клавиши; A-код; иначе-0FFh
C815h
printHexByte
Вывести 16-ричное число на экран; A-число
C818h
printString
Вывод строки символов на экран, до нулевого байта; HL-начало строки
C81Bh
keyScan
Ввод кода нажатой клавиши; A-код; иначе-0FFh
C81Eh
getCursorPos
Получить координаты курсора в HL (координаты в пикселях)
C821h
setCursorPos
Установить координаты курсора из HL (координаты в пикселях)
0C824h
tapeLoad
Загрузить программу с ленты
0C827h
tapeSave
Сохранить программу на ленту
0C82Ah
calcCrc
Расчет контрольной суммы
C82Dh
printHexWord
Вывести 16-ричное число на экран; HL-число
C830h
getMemTop
Получить объем доступной памяти; HL-объем
C833h
setMemTop
Установить объем доступной памяти; HL-объем
C836h
printer
Вывод байта на принтер
C838h
-
Переход на 0C800h
C83Сh
reboot2
Запустить A:NC.COM (стандартную точку С800h можно изменить)
C83Fh
fileList
Получить список файлов
C842h
fileGetSetDrive
Получить/установить активное устройство
C845h
fileCreate
Создать файл
C848h
fileLoad
Загрузить файл по адресу из заголовка этого файла
C84Bh
fileDelete
Удалить файл
C84Eh
fileRename
Переименовать файл
C851h
fileLoadInfo
Загрузить информацию о файле
C854h
fileGetSetAddr
Получить/установить адрес загрузки файла
C857h
fileGetSetAttr
Получить/установить атрибуты файла
C85Ah
fileNamePrepare
Преобразовать имя файла во внутренний формат
C85Dh
fileExec
Запустить файл
C860h
installDriver
Установить драйвер накопителя
C863h
diskDriver
Драйвер выбранного диска
C866h
fileLoad2
Загрузить файл по адресу
Стандартные служебные ячейки
8FE1h
2
tapeError
Адрес куда происходит переход при ошибке чтения с ленты
8FE3h
2
tapeAddr
Адрес программы загруженной с ленты
8FE7h
2
charGen
Адрес альтернативного знакогенератора; адрес необходимо разделить на 8
8FE9h
1
cursorCfg
Внешний вид курсора (7 - бит видимость, 654 - положение, 3210 - высота)
8FEAh
1
koi8
0FFh=включен KOI-8, 0=включен KOI-7
8FEBh
1
escMode
Обработка ESC-последовательности
8FECh
1
keyLocks
Состояние клавиш CAPS LOCK и РУС/LAT
8FEFh
2
lastKey
Две последние нажатые клавиши
8FF1h
2
beep
Длительность и частота звукового сигнала
8FF4h
1
repeat
Задержка повтора клавиш
8FFAh
2
inverse
0=нормальный текст, 0FFFFh=инверсный текст
8FFCh
1
cursorY
Положение курсора по вертикали в пикселях
8FFDh
1
cursorX
Положение курсора по горизонтали в пикселях / 2
8FFEh
1
writeDelay
Скорость при записи на ленту
8FFFh
1
readDelay
Скорость при чтении с ленты
Карта памяти
0000h - 8FDEh
36830 байт
Свободная память
8FDFh - 8FFFh
33 байта
Системные переменные
9000h - BFFFh
12 Кб
Экран
C000h - СFFFh
4 Кб
DOS.SYS (после ~CE94h свободно)
D000h - E1FFh
4608 байт
NC.COM (после ~E11Bh свободно) / FORMAT.COM
E200h - FAFFh
6 Кб
Резерв под драйвера
FB00h - FDFFh
768 байт
Дисковый буфер
FF00h - FF81h
130 байт
Коммандная строка. Заполняется fileExec
FF82h - FFC0h
62 байта
Стек
FFC0h - FFEFh
32 байта
DOS.SYS
FFD0h - FFFFh
32 байта
Оборудование
Формат файловой системы FAT8
Оригинальная ОС поддерживает лишь 64 Кб ДОЗУ и при запуске с большим объемом зависнет. Это происходит из за того, что в регистр страницы ДОЗУ записывается случайное значение.
Исправленная ОС (опция BIG_MEM) по умолчанию 7 первых страниц ДОЗУ отображает как накопители B-H.</p>
Операционная система адресует диски 256 байтными блоками. А так как последние 64 байта адресного пространства всегда занимают основное ОЗУ и устройства, то целых блоков получается 255.
То есть операционная система не использует последние 192 байта памяти. Только NC.COM в конце нулевой страницы (FF00h-FF0Ah) хранит своё состояние.
Файловая система подобна FAT. Накопитель максимальным объемом 64 КБ разбит на 256 блоков по 256 Кб.
Первые 4 блока содержат служеюную информацию. Нулевой блок таблицу FAT, следующие 4 блока - каталог.
Таблица FAT содержит 256 чисел. Число 5 по адресу 8, значит что за 8 блоком следует читать 5-ый блок. Последний блок замыкается сам на себя, то есть у последнего блока в ячейке 7 должно быть число 7. Свободным блокам в таблице FAT соответствует число 0. Первые 4 числа в таблице не используются, как и не используются блоки с нмоерами 0-3 для хранения файлов.
Каталог находящийся в блоках 1-3 содержит список файлов. 48 файлов по 16 байт на каждый. Если первый байт имени файла FFh, значит файл не существует.
Структура записи следующая:
6 байт - имя файла
3 байта - расширение файла
1 байт - атрибуты файла
2 байт - адрес загрузки файла
2 байт - длина файла - 1
1 байт - ?
1 байт - первый кластер файла
После включения компьютера процессор начинает выполнять программу с начала ПЗУ. Эта ПЗУ имеет такой же формат как и ДОЗУ и представлена в системе диском A:
Первые 4 байта ПЗУ, то есть таблицы FAT содержат команду перехода. Сам же загрузчик может обычно размещаться в конце каталога. Если первый символ имени файла FF, то остальные 15 байт записи могут содержать произвольные данные.
Но загрузчик можно размещать в любом блоке, который может быть даже помечен как свободный. При использовании неизменного ПЗУ это никогда не приведет к ошибке.
В оригинальной системе ПЗУ по адресам 800h-FFFh олжно содержать знакогенератор. Перед выводом каждого символа на экран, этот символ будет копировать из ПЗУ в ОЗУ. Что не только медленно, но и не позволяет отвязать систему от ПЗУ.
И еще хвала TASM :)
Здорово, что в tasm есть макросы из языка Си. Код получается на порядок понятнее.
#define G_WINDOW(X,Y,W,H) .db 2, Y, 90h+(X>>3), H-6, (W>>3)-2
#define G_HLINE(X,Y,W) .db 1, Y, 90h+(X>>3), (((X&7)+W+7)>>3)-2, 0FFh>>(X&7), (0FF00h>>((W+X)&7)) & 0FFh
#define G_VLINE(X,Y,H) .db 3, Y, 90h+(X>>3), H, 80h>>(X&7)
g_filePanel: G_WINDOW(0, 0, 192, 230) ; было 2, 0, 90h, 0E0h, 16h
G_HLINE(4, 208, 184) ; было 1, 0D0h, 90h, 16h, 0Fh, 0F0h
G_VLINE(96, 3, 205) ; было 3, ?, 9Ch, 0CDh, 80h
.db 0
g_infoPanel: G_WINDOW(0, 0, 192, 230) ; было 2, 0, 90h, 0E0h, 16h
G_HLINE(4, 31, 184) ; было 1, 1Fh, 90h, 16h, 0Fh, 0F0h
|
Просмотров: 345 |
Добавил: supoinclus
| Рейтинг: 0.0/0 |
| |
 | |  |
|
Вход на сайт |
|
 |
Поиск |
|
 |
Календарь |
|
 |
Архив записей |
|
 |
|