Black Shaped Box

[English translation may be some day]

К любому разработчику на OpenGL периодически приходит Малевич. Происходит это неожиданно и дерзко, ты просто запускаешь проект и видишь черный квадрат вместо чудесного рендера:

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

Используй инструменты

Для отладки OpenGL мне помогли два инструмента: renderdoc и apitrace. Renderdoc – инструмент для отладки процесса рендеринга OpenGL, просматривать можно все – вертексы, шейдеры, текстуры, отладочные сообщения от драйвера. Apitrace – инструмент для трейсинга вызовов графического API, делает дамп вызовов и показывает аргументы. Также есть великолепная возможность сравнивать два дампа через wdiff (или без него, но не так удобно)

Проверяй с кем работаешь

У меня есть операционная система Ubuntu 16.10 со старыми зависимостями SDL2, GLM, assimp, GLEW. В последней версии Ubuntu 18.04 я получаю сборку игры Death-Mask которая ничего не показывает на экране (только черный квадрат). При использовании chroot и сборке в 16.10 я получаю рабочую сборку игры с графикой.


Похоже что-то сломалось в Ubuntu 18.04

LDD показал линковку к идентичным библиотекам SDL2, GL. Прогоняя нерабочий билд в renderdoc, я увидел мусор на входе в вертексный шейдер, но мне нужно было более солидное подтверждение. Для того чтобы разобраться в разнице между бинариками я прогнал их оба через apitrace. Сравнение дампов показало мне что сборка на свежей убунте ломает передачу матриц перспективы в OpenGL, фактически отправляя туда мусор:

Матрицы собираются в библиотеке GLM. После копирования GLM из 16.04 – я снова получил рабочий билд игры. Проблема оказалась в разнице инициализации единичной матрицы в GLM 9.9.0, в ней необходивно явно указывать аргумент mat4(1.0f) в конструкторе. Поменяв инициализацию и отписав автору библиотеки, я принялся делать тесты для FSGL. в процессе написания которых я обнаружил недоработки в FSGL, их опишу далее.

Определись ты кто по жизни

Для корректной работы с OpenGL нужно в добровольно принудительном порядке запросить контекст определенной версии. Так это выглядит для SDL2 (проставлять версию нужно строго до инициализации контекста):

    SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, 2);
    SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE );   

Например Renderdoc не работает с контекстами ниже 3.2. Хочется отметить что после переключения контекста высока вероятность увидеть тот самый черный экран. Почему?
Потому что контекст OpenGL 3.2 обязательно требует наличие VAO буфера, без которого не работают 99% графических драйверов. Добавить его легко:

    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);

Не спи, замерзнешь

Также я встретился с интересной проблемой на Kubuntu, вместо черного квадрата у меня выводился прозрачный, а иногда все рендерилось корректно. Решение этой проблемы я нашел на Stack Overflow:
https://stackoverflow.com/questions/38411515/sdl2-opengl-window-appears-semi-transparent-sometimes

В коде тестового рендера FSGL тоже присутствовал sleep(2s); Так вот на Xubuntu и Ubuntu я получал корректный рендер и отправлял приложение спать, однако на Kubuntu я получил прозрачный экран в 80% случаев запуска из Dolphin и 30% запусков и терминала. Для решения данной проблемы я добавил рендеринг в каждом кадре, после опроса SDLEvent, как это рекомендуется делать в документации.

Код теста:
https://gitlab.com/demensdeum/FSGLtests/blob/master/renderModelTest/

Поговори с драйвером

OpenGL поддерживает канал связи между приложением и драйвером, для его активации нужно включить флаги GL_DEBUG_OUTPUT, GL_DEBUG_OUTPUT_SYNCHRONOUS, проставить оповещение glDebugMessageControl и привязать каллбек через glDebugMessageCallback.
Пример инициализации можно взять здесь:
https://github.com/rock-core/gui-vizkit3d/blob/master/src/EnableGLDebugOperation.cpp

It grows!

[English translation may be some day]

В данной заметке я расскажу о своих злоключениях с умными указателями shared_ptr. После реализации генерации следующего уровня в своей игре Death-Mask, я заметил утечку памяти. Каждый новый уровень давал прирост + 1 мегабайт к потребляемой оперативной памяти. Очевидно что какие-то объекты оставались в памяти и не освобождали ее. Для исправления данного факта необходимо было реализовать корректную реализацию ресурсов при перегрузке уровня, чего видимо сделано не было. Так как я использовал умные указатели, то вариантов решения данной задачи было несколько, первый заключался в ручном отсмотре кода (долго и скучно), второй же предполагал исследование возможностей дебагера lldb, исходного кода libstdc++ на предмет возможности автоматического отслеживания изменений счетчика.

В интернете все советы сводились к тому чтобы вручную отсматривать код, исправить и бить себя плетями после нахождения проблемной строчки кода. Также предлагалось реализовать свою собственную систему работы с памятью, как это делают все крупные проекты разрабатываемые еще с 90-х и нулевых, до прихода умных указателей в стандарт C++11. Мною была предпринята попытка использовать брейкпоинты на конструкторе копии всех shared_ptr, после нескольких дней ничего дельного не получилось. Была идея добавить логирование в библиотеку libstdc++, однако трудозатраты (о)казались чудовищными.


Cowboy Bebop (1998)

Решение пришло мне в голову внезапно в виде отслеживания изменений приватной переменной shared_ptr – use_count. Сделать это можно с помощью встроенных в lldb ватчпоинтов (watchpoint) После создания shared_ptr через make_shared, изменения счетчика в lldb можно отслеживать с помощью строки:

watch set var camera._M_refcount._M_pi->_M_use_count

Где “camera” это shared_ptr объект состояние счетчика которого необходимо отследить. Конечно внутренности shared_ptr будут различаться в зависимости от версии libstdc++, но общий принцип понять можно. После установки ватчпоинта запускаем приложения и читаем стектрейс каждого изменения счетчика, потом отсматриваем код (sic!) находим проблему и исправляем. В моем случае объекты не освобождались из таблиц-кешэй и таблиц игровой логики. Надеюсь данный метод поможет вам разобраться с утечками при работе с shared_ptr, и полюбить этот инструмент работы с памятью еще больше. Удачного дебага.

TensorFlow Simple Example

[English translation may be some day]

Представляю вашему вниманию простейший пример работы с фреймворком для работы с Deep Learning – TensorFlow. В этом примере мы научим нейросеть определять положительние, отрицательные числа и ноль. Установку TensorFlow и CUDA я поручаю вам, эта задачка действительно не из легких)

Для решения задач классификации используются классификаторы. TensorFlow имеет несколько готовых высокоуровневых классификаторов, которые требуют минимальной конфигурации для работы. Сначала мы потренируем DNNClassifier с помощью датасета с положительными, отрицательными числами и нулем – с корректными “лейблами”. На человеческом уровне датасет представляет из себя набор чисел с результатом классификации (лейблами):

10 – положительное
-22 – отрицательное
0 – ноль
42 – положительное
… другие числа с классификацией

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

import tensorflow
import itertools
import random

from time import time

class ClassifiedNumber:
    
    __number = 0
    __classifiedAs = 3
    
    def __init__(self, number):
        
        self.__number = number
        
        if number == 0:
            self.__classifiedAs = 0 # zero
            
        elif number > 0:
            self.__classifiedAs = 1 # positive
            
        elif number < 0:
            self.__classifiedAs = 2 # negative
            
    def number(self):
        return self.__number
    
    def classifiedAs(self):
        return self.__classifiedAs
    
def classifiedAsString(classifiedAs):
    
    if classifiedAs == 0:
        return "Zero"
    
    elif classifiedAs == 1:
        return "Positive"
    
    elif classifiedAs == 2:
        return "Negative"

def trainDatasetFunction():
    
    trainNumbers = []
    trainNumberLabels = []
    
    for i in range(-1000, 1001):    
        number = ClassifiedNumber(i)
        trainNumbers.append(number.number())
        trainNumberLabels.append(number.classifiedAs())
    
    return ( {"number" : trainNumbers } , trainNumberLabels )

def inputDatasetFunction():
    
    global randomSeed
    random.seed(randomSeed) # to get same result
    
    numbers = []
    
    for i in range(0, 4):
        numbers.append(random.randint(-9999999, 9999999))
    
    return {"number" : numbers }
    
def main():
    
    print("TensorFlow Positive-Negative-Zero numbers classifier test by demensdeum 2017 (demensdeum@gmail.com)")
    
    maximalClassesCount = len(set(trainDatasetFunction()[1])) + 1
    
    numberFeature = tensorflow.feature_column.numeric_column("number")
    classifier = tensorflow.estimator.DNNClassifier(feature_columns = [numberFeature], hidden_units = [10, 20, 10], n_classes = maximalClassesCount)
    generator = classifier.train(input_fn = trainDatasetFunction, steps = 1000).predict(input_fn = inputDatasetFunction)
    
    inputDataset = inputDatasetFunction()
    
    results = list(itertools.islice(generator, len(inputDatasetFunction()["number"])))
    
    i = 0
    for result in results:
        print("number: %d classified as %s" % (inputDataset["number"][i], classifiedAsString(result["class_ids"][0])))
        i += 1

randomSeed = time()

main()

Все начинается в методе main(), мы задаем числовую колонку с которой будет работать классификатор – tensorflow.feature_column.numeric_column(“number”) далее задаются параметры классификатора. Описывать текущие аргументы инициализации бесполезно, так как API меняется каждый день, и обязательно нужно смотреть документацию именно установленной версии TensorFlow, не полагаться на устаревшие мануалы.

Далее запускается обучение с указанием на функцию которая возвращает датасет из чисел от -1000 до 1000 (trainDatasetFunction), с правильной классификацией этих чисел по признаку положительного, отрицательного либо нуля. Следом подаем на вход числа которых не было в обучающем датасете – случайные от -9999999 до 9999999 (inputDatasetFunction) для их классификации.

В финале запускаем итерации по количеству входных данных (itertools.islice) печатаем результат, запускаем и удивляемся:

number: 4063470 classified as Positive
number: 6006715 classified as Positive
number: -5367127 classified as Negative
number: -7834276 classified as Negative

iT’S ALIVE

Честно говоря я до сих пор немного удивлен что классификатор *понимает* даже те числа которым я его не обучал. Надеюсь в дальнейшем я разберусь более подробно с темой машинного обучения и будут еще туториалы.

GitLab:
https://gitlab.com/demensdeum/MachineLearning

Ссылки:
https://developers.googleblog.com/2017/09/introducing-tensorflow-datasets.html
https://www.tensorflow.org/versions/master/api_docs/python/tf/estimator/DNNClassifier

Broken Coin Bitcoin

[English translation may be some day]

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

Уязвимый центр

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


Принцип блок-чума от Елкина

Для того чтобы блокчейн работал, нужно сделать так чтобы каждый пользователь скачивал последнюю копию базы данных блокчейна, и работал с ней по определенным правилам. К таким правилам относится реализация принципа майнинга биткоина, получение процента от каждой транзакции при подтверждении (transaction fee) передачи средств с одного кошелька на другой. Пользователь не может нарисовать себе 1000000 биткоинов и купить на них что-то, т.к. у других пользователей количество денег на его счету будет неизменным. Также исключен вариант со снятием средств с чужого кошелька только внутри своей базы данных т.к. это изменение не будет отражено у других пользователей биткоина, и будет проигнорировано.
Уязвимость текущей реализации заключается в том что биткоин кошелек находится на сервере github что полностью перекрывает рекламные лозунги о децентрализации. Без загрузки кошелька из единого центра – сайта разработчика, невозможно работать с биткоином, тоесть в любой момент разработчики имеют полный контроль над сетью. Таким образом, сама технология блокчейн является децентрализованной, но клиент для работы с сетью загружается из единого центра.
Сценарий атаки  – допустим в кошелек добавлен код для снятия всех средств и обналичивания на счет третьих лиц, после этого любой пользователь последней версии кошелька потеряет все биткоины автоматически (без возможности восстановления). Сомневаюсь что многие владельцы кошелька проверяют и собирают его из исходного кода, поэтому последствия такой атаки затронут большинство пользователей.

Решает большинство

Блокчейн является децентрализованной p2p сетью, подтверждением всех операций занимаются сами пользователи в автоматическом режиме. Сценарий атаки  – необходимо получить 51% сети для того чтобы игнорировать подтверждения оставшихся 49%, после этого атакующий получает полный контроль над биткоином/блокчейном. Этого можно добиться подключив вычислительные мощности перекрывающие остальных. Этот сценарий атаки известен как 51% attack.

Угадай меня если сможешь

При первом запуске кошелька, компьютер генерирует пару – приватный и публичный ключ для обеспечения своей корректной работы. Уникальность данных ключей крайне высока, однако есть вариант сгенерировать ключи с помощью кодового слова – так называемый “brain wallet“. Человек хранит ключи у себя в голове, ему не нужно делать бекап файла wallet.dat, т.к. в любой момент ключи можно будет перегенерить с помощью данного кодового слова. Сценарий атаки – злоумышленник подбирает или узнает кодовое слово, генерирует пару приватный-публичный ключ и получает контроль над кошельком.

Просто скопируй

Пара приватный-публичный ключ содержится в файле wallet.dat. Любое программное обеспечение имеющее доступ к данному файлу – имеет доступ к кошельку биткоин. Защитой от такого нападения служит добавление кодового слова, которое должен будет помнить и вводить пользователь, для всех операций с кошельком. После добавления кодового слова, злоумышленнику нужно будет иметь wallet.dat и кодовое слово для получения полного контроля.
Также стоит добавить что при вводе кодового слова оно попадает в память компьютера, таким образом любые уязвимости аппаратные и/или программые позволяющие читать *чужую* память позволят прочитать и это кодовое слово вирусному программному обеспечению.

Ошибка системы

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

WebGL + SDL + Emscripten

[Translation may be some day]

В итоге я портировал Мику на WebGL, с помощью SDL 1 и Emscripten.

http://demensdeum.com/demos/mikuWebGL/

Дальше я опишу что нужно было изменить в коде чтобы сборка в JavaScript завершилась успешно.

  1. Использовать SDL 1 вместо SDL 2. На данный момент существует порт SDL 2 для emscripten, однако использовать встроенный в emscripten SDL 1 я посчитал более целесобразным. Инициализация контекста происходит не в окне, а с помощью SDL_SetVideoMode и флага SDL_OPENGL. Отрисовка буфера производится командой SDL_GL_SwapBuffers()
  2. Из-за особенностей выполения циклов в JavaScript – рендеринг вынесен в отдельную функцию и его периодический вызов проставляется с помощью функции emscripten_set_main_loop
  3. Также сборку нужно осуществлять с ключом “-s FULL_ES2=1
  4. Пришлось отказаться от библиотеки assimp, от загрузки модели из файловой системы, от загрузки текстуры с диска. Все необходимые буферы были прогружены на деcктоп версии, и прокинуты в c-header файл для сборки с помощью emscripten.

Код:
https://github.com/demensdeum/OpenGLES3-Experiments/tree/master/9-sdl-gles-obj-textured-assimp-miku-webgl/mikuWebGL

Статьи:
http://blog.scottlogic.com/2014/03/12/native-code-emscripten-webgl-simmer-gently.html
https://kripken.github.io/emscripten-site/docs/porting/multimedia_and_graphics/OpenGL-support.html

Модель:
https://sketchfab.com/models/7310aaeb8370428e966bdcff414273e7