суббота, 27 февраля 2010 г.

Твой путь

Данную статью я посвящаю трем категориям людей: евангелистам Microsoft, работодателям и специалистам, связавшим свою деятельность с платформой dotNet (далее будем подразумевать язык C#). На написание данного опуса меня вдохновило очередное перечитывание книги «CLR via C#» Джеффри Рихтера. Сейчас объясню почему.

На данном историческом этапе людям, позиционирующим себя как dotNet разработчики, зачастую приходится отвечать на вопросы подобные этим:

- почему вы выбрали C#, а не C++?
- чем C# лучше C++?
- а вы знаете, что C# медленнее C++? (очередной «капитан очевидность»)

Работодателей выделю в отдельный абзац, по причине существования тенденции непонимания ими основополагающих факторов. В моей практике не так давно был веселый случай, когда перспективный работодатель, зная мое владение C++, предложил работу, которая подразумевала бы мой абсолютный переход обратно на C++. На что, получив мой отрицательный ответ, мотивированный нежеланием делать шаг назад, начал дискуссию в лучших традициях “Holy wars C++ vs C#”. Но война не сложилась. Видимо сказалось мое накопленное к тому моменту недовольство с оплатой предыдущих проектов, в итоге гражданин дуется на меня до сих пор, так и не поняв мотивов и побуждений движущих мною. По этому, уважаемые работодатели, на подобные предложения я хотел бы ответить Вам цитированием слов абсолютного авторитета в нашей области Джеффри Рихтера (цитата приведена далее по тексту и выделена курсивом, особенно ценное выражение выделено жирным курсивом). И если в ответе на ваше предложение вы нашли ссылку на данную статью, то мой ответ скорее отрицательный, чем положительный.

Так же постоянно находятся желающие, хотите вы этого или нет, поспорить с вами, развязать священную войну и доказать что именно C++ есть единственный и ничего кроме него. От серьезного человека Вы вряд ли такое услышите и скорее всего найдутся более интересные темы для беседы с ним. Разработчики с такими ситуациями сталкиваются в своей повседневной деятельности, в разговорах с коллегами, желающими выделится на вашем фоне, и т.д., евангелисты на встречах, семинарах, выступлениях, докладах. У каждого из нас есть свой багаж аргументов и оборонительных тактик, что является естественным. Не всегда выгодно игнорировать человека. Иногда обстоятельства складываются так, что остается только два пути пасовать или побеждать. Для победы, желательно не затягивая дебаты, нужны веские и достаточные аргументы. Какими же эти аргументы должны быть? Возьмем за основу три принципа:

1. Козьма Прутков: «Зри в корень» -> определи - что для оппонента является точкой опоры.
2. Основополагающее правило РРБ: «Лишить противника равновесия максимально эффективно и быстро» -> соответственно выбить почву из под ног, лишив опоры.
3. Геометрический метод: «Доказательство от обратного» -> все действия нужно провернуть с позитивом, не стандартно и оригинально.

Исходя из выше сказанного и того, что ваш оппонент ярко выраженный фанат С++, а так же человек не желающий развиваться дальше достигнутой ступеньки. Возьмем за основу нашей тактики следующую последовательность вопросов:

1. Какая книга и какого автора является для вас библией в вашей профессиональной деятельности как C++ программиста? – естественно автор будет Джеффри Рихтер или вы общаетесь не с программистом.
2. Что из себя представляет Джеффри Рихтер как специалист и кем он является: например по отношению к таким компаниям как Intel, DreamWorks и Microsoft? – нужно понимать, что данным вопросом мы преследуем сразу несколько целей. Во-первых: он подготавливает ситуацию к финальному ходу. Во-вторых: есть возможность заработать бонусные балы себе в глазах свидетелей (в зале например), если человек не владеет информацией глубже литературной деятельности автора. В-третьих: мы получаем добровольное признание оппонента в том, что данный автор является для него авторитетом с абсолютной степенью значимости. «… кто его тронет, он же памятник! …» - Рихтер выступал в роли консультанта для таких компаний как Intel, DreamWorks и Microsoft.
3. И контрольный вопрос: Как вы относитесь к высказыванию Джеффри Рихтера в одной из относительно недавних его книг? А конкретно:

«…Уже несколько лет я использую .NET Framework и должен сказать с уверенностью, что ни за что не вернусь к устаревшим технологиям абстрагирования и способам разработки ПО. И, если меня заставят, я предпочту сменить профессию! Вот как трудно отвыкать от хорошего. Честно говоря, вспоминая, чего стоило создавать приложения с использованием старых технологий, я просто не могу представить, как разработчикам вообще удавалось так долго создавать работающее ПО.»
Джеффри Рихтер, книга «CLR via C# программирование на платформе .NET FRAMEWORK на языке С#» 2-е издание, Введение, страница XIV.

За сим прощаюсь с вами. Вешаю на свой щит, как икону, фото и цитату Рихтера и со словами «изыйдите нечистые» продолжу свой путь.

P.S. Данная статья является результатом стечения обстоятельств и соответствующего настроения )

P.S.S. За статьей последовала переписка с Джеффри Рихтером, которую я привожу с его на то разрешения (Разговорный английский ни когда не был моей сильной стороной):

- Good day Jeffrey. I'm Russian developer and I adore Your books, that has directed me on writing the following article: «Твой путь». This article writing on Russian language. I be very happy if see You comment, if You have possibility.
- Добрый день Джеффри. Я являюсь русским разработчиком и достаточно сильно уважаю ваши книги, что вдохновило меня на написание следующей статьи: «Твой путь». Данная статья написана на русском языке. Я был бы очень счастлив увидеть Ваши комментарии, если Вас это не обременит.
-- Dmitry Timofeev

- I read your post via a translator which didn't do a great job of translating the text but I think I get the general gist idea of what you are trying to say. I have to say that C# is a much more programmer-friendly environment than C++ is. But, I also have to say that C++ can do some things that C# cannot do: more control over your system, better error recovery and, in some cases, better performance. I don't want to give the impression that C# is the solution for every problem. There are some problems that C++ is better suited for: operating systems and database servers come immediately to mind. Personally, I'm in a position where I would not go back to C++ development but there is still a needs for C++ developers as C# can't do everything.
- Я прочитал Ваш пост через переводчик, который не делал большой работы по переводу текста, но думаю, что понял то, что Вы пытаетесь сказать. Должен сказать, что C# является намного более благоприятной для программиста средой, чем C++. Но также должен сказать, что C++ может сделать некоторые вещи лучше, чем C# : возможен больший контроль над системой, лучшее восстановление после ошибок и, в некоторых случаях, лучшая производительность.
Я не хочу создать впечатление, что C# - решение для каждой проблемы. Есть некоторые задачи, для решения которых лучше подходит С++: на ум сразу приходят операционные системы и серверы баз данных. Лично я не хотел бы вернуться назад к разработке на C++, но есть все еще потребность в C++ разработчиках, поскольку C# не может сделать всего.
-- Jeffrey Richter (http://Wintellect.com)

- Thank's from comments. Your words once again emphasize true meaning my article. My opinion comply with You. I have attitude to programing in old school style. Including programming on periods: assembler Z80 (XZ Spectrum), c++, c++ and OpenGL, and in present time C#, particularly pay own attention XNA, it's explain my relation with quoting You words. I ask You permit publication in my blog from You mail comments.
- Спасибо за комментарий. Ваши слова еще раз подтверждают мысль моей статьи. Наши мнения совпадают. Я отношусь к программистам которых относят к старой школе (old school). Прохождение через периоды программирования на: ассемблере Z80 (ZX Spectrum), c++, c++ и OpenGL, и в настоящее время C #, особенно много уделяя собственное внимание XNA, может объяснить мое отношении к цитированию Ваших слов. Я прошу Вашего разрешения публикации Ваших комментариев в моем блоге.
-- Dmitry Timofeev

- Yes you may post what i sent you.
- Да Вы можете опубликовать то, что я послал Вам.
-- Jeffrey Richter (http://Wintellect.com)

воскресенье, 21 февраля 2010 г.

#1 XnaDevRuEngine - 3d Max Export Plugin

Заработал мой плагин экспорта из 3d Max. Были сложности с взаимодействием костей, но упорство, блокнот и карандаш все решили.

четверг, 4 февраля 2010 г.

#0 XnaDevRuEngine

Мои коллеги, с которыми меня объединяет деятельность в рамках ресурса xnadev.ru, и я, начали работу над 3D движком XnaDevRuEngine. Если не брать в расчет наработки по модулю интерфейса (forms), которые ранее существовали как самостоятельный проект, то следующее видео можно считать первым, демонстрирующим XnaDevRuEngine:

Сейчас частично готовы модули SceneTree и прототип ResorceManager.

пятница, 18 сентября 2009 г.

четверг, 10 сентября 2009 г.

С#+XNA. В погоне за fps. I

Краткое содержание:

- Введение;
- Часть 1-я. Новичкам;
- Часть 2-я. Та же песня с Microsoft'ом;
- Часть 3-я философская. Охотник или жертва;
- Заключение.


Введение.

Данная статья является попыткой посмотреть в корень проблемы. Проблемы достижения высоких показателей fps с точки зрения построения C# кода не касаясь при этом XNA раздела 3D, относящегося к непосредственным инструкциям видеокарте.


Часть 1-я. Новичкам.

Мои наблюдения основаны на постоянном присутствии в различных форумах и участии в различных проектах. Соль рассматриваемого мною вопроса заключается в использовании Fields и Properties членов классов и структур.
Для полноты ощущений, в моих изысканиях, меня интересовали следующие ссылочные типы:
- class;
- interface;
- object.
Сейчас мы просмотрим на время выполнения кода при различной организации доступа к членам класса.
Пример:


using System;
namespace TestObjectInterface
{
class Program
{
static Unit[] unit;
static object[] unitobj;
static iUnit[] uniti;

static int count;

static void Main(string[] args)
{
count = 10000000;
unit = new Unit[count];
unitobj = new object[count];
uniti = new iUnit[count];
Init();
double tf = TestFields();
double tp = TestProperties();
double tfo = TestFieldsObject();
double tpo = TestPropertiesObject();
double tpi = TestPropertiesInterface();
Console.WriteLine("testFields = "
+ tf.ToString() + " mc, k = 1");
Console.WriteLine("testProperties = "
+ tp.ToString() + " mc, k = "
+ (tp / tf).ToString());
Console.WriteLine("testFieldsObject = "
+ tfo.ToString() + " mc, k = "
+ (tfo / tf).ToString());
Console.WriteLine("testPropertiesObject = "
+ tpo.ToString() + " mc, k = "
+ (tpo / tf).ToString());
Console.WriteLine("testPropertiesInterface = "
+ tpi.ToString() + " mc, k = "
+ (tpi / tf).ToString());
//Console.ReadKey();
}

static void Init()
{
for (int i = 0; i < count; i++)
{
unit[i] = new Unit(i, i * 2);
uniti[i] = (iUnit)unit[i];
unitobj[i] = (object)unit[i];
}
}

static double TestFields()
{
DateTime dtStart = DateTime.Now;
int n;
for (int i = 0; i < count; i++)
{
n = unit[i].x + unit[i].y; // 0
n = unit[i].x + unit[i].y; // 1
n = unit[i].x + unit[i].y; // 2
n = unit[i].x + unit[i].y; // 3
n = unit[i].x + unit[i].y; // 4
n = unit[i].x + unit[i].y; // 5
n = unit[i].x + unit[i].y; // 6
n = unit[i].x + unit[i].y; // 7
n = unit[i].x + unit[i].y; // 8
n = unit[i].x + unit[i].y; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}

static double TestProperties()
{
DateTime dtStart = DateTime.Now;
int n;
for (int i = 0; i < count; i++)
{
n = unit[i].X + unit[i].Y; // 0
n = unit[i].X + unit[i].Y; // 1
n = unit[i].X + unit[i].Y; // 2
n = unit[i].X + unit[i].Y; // 3
n = unit[i].X + unit[i].Y; // 4
n = unit[i].X + unit[i].Y; // 5
n = unit[i].X + unit[i].Y; // 6
n = unit[i].X + unit[i].Y; // 7
n = unit[i].X + unit[i].Y; // 8
n = unit[i].X + unit[i].Y; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}

static double TestFieldsObject()
{
DateTime dtStart = DateTime.Now;
int n;
for (int i = 0; i < count; i++)
{
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 0
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 1
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 2
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 3
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 4
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 5
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 6
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 7
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 8
n = ((Unit)unitobj[i]).x + ((Unit)unitobj[i]).x; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}

static double TestPropertiesObject()
{
DateTime dtStart = DateTime.Now;
int n;
for (int i = 0; i < count; i++)
{
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 0
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 1
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 2
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 3
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 4
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 5
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 6
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 7
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 8
n = ((Unit)unitobj[i]).X + ((Unit)unitobj[i]).Y; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}

static double TestPropertiesInterface()
{
DateTime dtStart = DateTime.Now;
int n;
for (int i = 0; i < count; i++)
{
n = uniti[i].iX + uniti[i].iY; // 0
n = uniti[i].iX + uniti[i].iY; // 1
n = uniti[i].iX + uniti[i].iY; // 2
n = uniti[i].iX + uniti[i].iY; // 3
n = uniti[i].iX + uniti[i].iY; // 4
n = uniti[i].iX + uniti[i].iY; // 5
n = uniti[i].iX + uniti[i].iY; // 6
n = uniti[i].iX + uniti[i].iY; // 7
n = uniti[i].iX + uniti[i].iY; // 8
n = uniti[i].iX + uniti[i].iY; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}
}

interface iUnit
{
int iX {get;}
int iY {get;}
}

public class Unit : iUnit
{
public int x;
public int X { get { return x; } }
int iUnit.iX { get { return x; } }

public int y;
public int Y { get { return y; } }
int iUnit.iY { get { return y; } }

public Unit(int inX, int inY)
{
x = inX;
y = inY;
}
}
}

Я не буду уделять внимание коду, пример достаточно детский. Скажу только то, что по 10 инструкций в каждом цикле сделано для контрастного выделения времени выполнения тела цикла, от самой инструкции цикла.
У меня программа отработала следующим образом:


На мой взгляд все достаточно наглядно. Самый скоростной способ это естественно обращение к полю класса на прямую.
А теперь поясню к чему рассказал и продемонстрировал прописную истину. Всем так же известно, что в разработке 3D, в отличие от бизнес, приложений особенно ярко выражена погоня за скоростью. Но тем не менее, специалисты начинающие(!) работать с XNA, уже как правило, достаточно уверенно себя ощущают в области C# разработки. Вот в этом то и заключается основной казус. Который объясняется тем фактом, что вся справочная и обучающая литература пестрит примерами описывающими доступ к полям классов через свойства. В следствии чего ребята начинают автоматически(!) применять данный подход во всех случаях. И там где это действительно необходимо, и где не очень. Да это хороший тон(!). Удобно перехватывать обращения к полю класса, обрабатывать возникающие при этом исключительные ситуации. Но не подходит для применения в 3D(!!!).
Не всегда можно быстро выдать столько информации по данному вопросу очередному собеседнику. Да и сейчас я описал вопрос не за пять минут, но в дальнейшем могу с легкостью ссылаться на себя :).


Часть 2-я. Та же песня с Microsoft'ом.

Не для кого не секрет, что я люблю поковырять рефлектором все, что меня хотя бы мало-мальски интересует. Добавлю то, что порой это приносит больше информации, чем например чтение непосредственно MSDN. Поэтому в довесок к первой части я решил привести один(!) пример из библиотеки XNA.
Рассмотрим XNA 3.1, виндовую библиотеку Microsoft.Xna.Framework.dll,
одноименный namespace, и пусть подопытным будет структура Vector3.
Вот часть кода данной структуры, в рамках которой мы сейчас и пообщаемся:


public struct Vector3
{
public float X;
public float Y;
public float Z;
...
private static Vector3 _one;
public static Vector3 One
{
get
{
return _one;
}
}
...
public Vector3(float x, float y, float z)
{
this.X = x;
this.Y = y;
this.Z = z;
}
...
static Vector3()
{
...
_one = new Vector3(1f, 1f, 1f);
...
}
}

К полям X, Y и Z вопросов нет. Но обращаю ваше внимание на свойство One. По логике One возвращает единичный вектор постоянного значения. А вот тут по подробнее. Разум цепляется за формулировку "постоянное значение". Читаем MSDN и выясняем, что переменная или поле с постоянным значением может быть объявлена при помощи двух ключевых слов модификаторов доступа, которыми являются const и readonly. На всякий случай поясню формулировкой, переменная или поле постоянного значения бывает двух видов:
- const -> применяется при объявления полей и локальных переменных, постоянное значение присваивается на этапе компиляции, исключительно(!) при объявлении;
- readonly -> применяется при объявления полей, значение может присваивается как при объявлении, так и в конструкторе данного класса или структуры.
Но речь идет о поле постоянного значения которое не возможно рассчитать при компиляции, что накладывает ряд ограничений и в соответствии с рассматриваемым нами случаем, расширим формулировки:
- Модификатор static не допускается в объявлении константы;
- значение константы должно быть полностью вычислено во время компиляции;
- Единственными возможными значениями для констант ссылочных типов являются string и null;
Ясное дело на static ставим крест, он однозначно не подходит. А вот readonly наш размерчик! И как нельзя лучше вписывается в нашу ситуацию. Потому, заранее уже зная куда нас это приведет, предлагаю по тестировать и сравнить время чтения значения из свойства стандартной структуры с чтением значения из иначе построенной структуры.
Смотрим пример:


using System;
using Microsoft.Xna.Framework;
namespace TestFields
{
class Program
{

static int count;

static void Main(string[] args)
{
count = 10000000;
Console.WriteLine("testStandartVector3 = " +
TestStandartVector3().ToString() +
" mc");
Console.WriteLine("testMyVector3 = " +
TestMyVector3().ToString() +
" mc");
}

static double TestStandartVector3()
{
DateTime dtStart = DateTime.Now;
Vector3 vec;
for (int i = 0; i < count; i++)
{
vec = Vector3.One; // 0
vec = Vector3.One; // 1
vec = Vector3.One; // 2
vec = Vector3.One; // 3
vec = Vector3.One; // 4
vec = Vector3.One; // 5
vec = Vector3.One; // 6
vec = Vector3.One; // 7
vec = Vector3.One; // 8
vec = Vector3.One; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}

static double TestMyVector3()
{
DateTime dtStart = DateTime.Now;
gVector3 vec;
for (int i = 0; i < count; i++)
{
vec = gVector3.One; // 0
vec = gVector3.One; // 1
vec = gVector3.One; // 2
vec = gVector3.One; // 3
vec = gVector3.One; // 4
vec = gVector3.One; // 5
vec = gVector3.One; // 6
vec = gVector3.One; // 7
vec = gVector3.One; // 8
vec = gVector3.One; // 9
}
DateTime dtEnd = DateTime.Now;
return (dtEnd - dtStart).TotalMilliseconds;
}
}

public struct gVector3
{
public float X;
public float Y;
public float Z;

public static readonly gVector3 One;

public gVector3(float x, float y, float z)
{
X = x;
Y = y;
Z = z;
}

static gVector3()
{
One = new gVector3(1, 1, 1);
}
}
}

Получаем вот такой вот результат:


Внимание вопрос(!): какая была необходимость реализовывать "постоянное значение" через свойство возвращающее значение приватного поля, если в сравнительном отношении скорости, с реализацией через модификатор доступа readonly, составляет разницу практически в(!) два раза? Мне не понятно, а вам? Буду очень признателен тому человеку, который вежливо мне объяснит.
Товарищам, которые планируют достаточно часто применять постоянные вектора:
- Zero;
- One;
- UnitX;
- UnitY;
- UnitZ;
- Up;
- Down;
- Right;
- Left;
- Forward;
- Backward.
Рекомендую не использовать стандартные, а реализовать самостоятельно, как в выше приведенном примере.
Мысль которую я хотел донести, звучит следующим образом:
- изучайте все средства, которые собираетесь применять в своих проектах;
- в результате изучения берите только лучшее и делайте наконец то сказку былью;
- не знание законов не освобождает от ответственности. Это про специалистов, которые по своей беспечности, не разбираясь, применяют все подряд, а в последствии сетуют на Microsoft. Хотя это говорит с плохой стороны не(!) о Microsoft.
- допуская то, что не смотря на "много букв", у читателей данной статьи мнения разойдутся. Я добавил третью часть. Читать рекомендую всем, только одним принять к сведению, а иным просто улыбнуться.


Часть 3-я философская. Охотник или жертва?

Жертвам своей не компетентности, которые любят плохо отзываться о Microsoft, посвящается.
Что, из себя, представляет понятие мода? Мода - в широком смысле этого слова, определяет стремление, того или иного человека, быть похожим на основное большинство. Хм, так то оно так, когда в меру, без фанатизма и крайностей. А так же не идет в разрез с законами эволюции, которые не зависимо от нас хранят приоритетное право "сильных" и "особенно удачных" особей выживать. К примеру обратная крайность это не формальные движения в нашем обществе.
Применяем выше описанное отступление к нашей основной теме. И так у нас есть три основных типа людей. Систематизируем их и результат оформим в виде структуры:
- ярко выражено не(!) довольные Microsoft'ом -> модные "жертвы". Да, уже более десяти лет считается модно и наверное круто так себя вести;
- фанаты Microsoft'а -> назовем их "панки", так веселее смотрится не формальное определение. Обычно эти люди либо являются специалистами, но узкого профиля, либо совсем не являются таковыми. Если кого то обидел, сообщите мне. Я подберу более мягкое определение.
- специалисты -> "охотники". Ни кому не покланяются, ни кого не ругают. Им некогда отвлекаться на разную чушь. Эти люди берут от жизни только лучшее, прекрасно понимая при этом как и где это лучшее применять, и как и где не стоит. Они же становятся MCP, MVP, "черными поясами" Intel, … , и другими почетными товарищами.


Заключение.

Ну вот и все. Всем перца насыпал. Развеялся. Можно дальше "охотиться" ... оговорился :) ... работать и точить свое мастерство.

суббота, 29 августа 2009 г.

XNA: Water video

Рекомендую смотреть в режиме HD 1024x768



В данном исполнении нет каустики. Но сейчас я заинтересовался этой темой, нагреб кучу инфы, изучаю ... пробую.
Пока мои изыскания выглядят так:



Т.е. простое вычисление в один (сильно тяжелые вычисления для real time) проход на CPU.

Пока делал первое видео, в глаза бросилась вот какая штука. Изначально видео с водичкой весило 2,092 ГБ, конвертировал при помощи Camtasia Studio и как то сильно быстро изменялся прогресс конвертирования. Посмотрел загрузку процессоров ... и о чудо.



Camtasia Studio умеет распределять вычисления на все доступные процессоры.
Конвертация прошла в real time режиме :). Очень порадовало!

понедельник, 24 августа 2009 г.