nabbla (nabbla1) wrote,
nabbla
nabbla1

Categories:

QuatCore+GPU: окончание кадра

Мы очень внимательно изучали, как работает связка QuatCore+GPU на тестовом изображении, раз за разом исправляли ошибки и наращивали длину входных и выходных буферов, "оптимизировали" программу, сократив её размер аж на 2 слова (с 83 до 81, т.е 162 байта) и скорость работы процентов на 10.

Прошли самые тяжелые строчки тестового изображения, 10-ю и 11-ю, где происходило максимально много всего: толпа мелких отрезочков, и с каждым индивидуально надо разобраться, новую точку добавить, старые точки удалить (перецепить), по пути выдавая задания на обработку "в реальном времени".

Дальше столь же внимательно наблюдать не хочется - "промотать" в самый конец, посмотреть, как себя ведёт GPU, когда знает, что полезные строки закончились в этом кадре, а ему продолжают идти "задания", и наконец посмотреть Memory dump...

Для начала быстренько глянем самую последнюю строку, 32-ю:




Как видно, заказан был всего один отрезок, длиной во всю строку, поэтому мы ждали пока вся строка пройдёт. Получили яркость 0xFFF (нулевая) и координата 0. Сравниваем её с порогом - и с чистой совестью прыгаем в @@MidCycle, где в Y сохраняем адрес текущей точки в списке (0x10, "левая фиктивная"), X "прыгает" на следующую точку, 0x12 ("правая фиктивная"),


а вот ссылка с неё уже ведёт в NULL, поэтому тут же прыгаем в @@FinalRange.

Проверяем номер строки: 0x20 (то есть, 32), но мы в своей бесконечной мудрости решили идти до 40-й строки, хоть их всего и 32. Поэтому из цикла мы пока не выходим, прибавляем единичку - и начинаем обрабатывать следующую, 33-ю строку!


И пока мы всё-таки "честно ждём" синхроимпульса и, возможно Back porch вслед за ним.

Увы, так и происходит на 33-й строке, затем на 34-й, и так далее. А всё потому, что я забыл поменять параметр RowCount в модуле QuatCoreFullGPUInput:


Там висело 720, как для "реального кадра" с китайской камеры высокой чёткости (1280х720). Сейчас поставил 32, но хочется сразу глянуть, как он поведёт себя при столь "круглом" числе. Вот строки кода, определяющие его работу по "строкам":

localparam YregWidth = `CLOG2(RowCount);
localparam YinitVal = (1 << YregWidth) - RowCount;	//возможно, на единичку ошиблись, потом проверим
wire Yfinished;

lpm_counter Ycounter (	.clock (clk),		
			.cnt_en (Hsync & (~Yfinished)),		
			.sset (Vsync),		
			.sload (1'b0),
			.data (),
			.Q (),
			.cout (Yfinished));			
	defparam
		Ycounter.lpm_direction = "UP",
		Ycounter.lpm_port_updown = "PORT_UNUSED",
		Ycounter.lpm_type = "LPM_COUNTER",
		Ycounter.lpm_width = YregWidth,
		Ycounter.lpm_svalue = YinitVal;

assign start = start_neg | start_oflo | start_normal | (Yfinished & UseRowCount);


Похоже, мы действительно ошиблись на единичку: если задать 32 строки, то счётчик выйдет 5-битным, от 0 до 31. И уже на строке с индексом 31 (последней) "загорится" Yfinished, из-за чего уже на ПОЛЕЗНОЙ СТРОКЕ начнётся трэш и угар.

Впрочем, сюда надо ещё добавить строки "сверху" от полезных, ведь как видно, счётчик сбрасывается по кадровому синхроимпульсу - и уже начинает считать. Поэтому можно не трогать код, но вместо 32 задать 43, поскольку ещё 10 строк, как мы считаем, расположено сверху, и только на строке, СЛЕДУЮЩЕЙ за последней полезной, нужно "включить" Yfinished.

Если мы так сделаем, то по приходу очередного синхроимпульса мы будем "спамить" выходной буфер каждый такт, очевидно переполняя его. Нужно к этому "морально приготовиться". И в то же самое время мы будем "опустошать" входной буфер, так что в нём "повиснет" бесконечное Underflow (попытка достать элемент из пустого буфера).

Пожалуй, можно это немножко подправить: добавим (~UFLO) в условие старта:

assign rdreq = start_neg | start_oflo | start_normal | (Yfinished & UseRowCount);
assign start = rdreq & (~UFLO); 


Вот чувствовал, что сигнал start (подаётся на сумматоры видеопроцессора, чтобы они занесли текущие результаты в выходной буфер, сбросили их - и начали работать по новому отрезку) в итоге разойдётся с rdreq (запрос на чтение из входного буфера)! Дело в том, что сигнал UFLO комбинаторно зависит от rdreq: он подаётся только когда rdreq=1, то есть пустой буфер сам по себе - это нормальная ситуация, а UFLO возникает когда мы уже пытаемся что-то из него вытащить. Поэтому включи мы UFLO в условие для rdreq - получили бы Combinational loop, по сути генератор. А так всё нормально.

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

Для полной уверенности этот код стоит убрать: когда мы выдаём start прямо каждый такт, там не может быть ничего другого, кроме нулей.

На будущее нас поджидает ещё одна подлянка: если теперь мы подадим задание ACQ VSync (дождаться кадрового синхроимпульса), то есть начнём работать со следующим кадром, это задание тоже мы быстренько "схрумкаем", чего происходить не должно! Поэтому наша весёлая жизнь должна прекратиться как только "нависнет" этот VSync. Что ж, это тоже можно:

assign rdreq = start_neg | start_oflo | start_normal | (Yfinished & UseRowCount & (~SyncOut[1]));


Теперь что-то может и получиться... Хотя всё равно это полуфабрикат: нам вообще-то нужны "светлые" и "тёмные" кадры, в "светлом" осветитель включён, а в "тёмном" выключен, и раз уж любая фотоприёмная матрица предпочитает "жить своей жизнью" (один раз её сбросил - и она начала выдавать данные в своём ритме), то нужно будет в такт ей управлять осветителем, и как-то отразить всё это дело здесь, на обработке. Хотя тут вопрос в одном бите всего. Видимо, будет отдельно задание на "светлый" кадровый синхроимпульс, и отдельно на "тёмный". Тут ещё могут быть хитрости со злополучным rolling shutter: когда поступит очередной кадровый синхроимпульс, экспозиция для верхней строки будет практически завершена, а для нижней только начнётся, и если в этот момент включить осветитель, в верхней части кадра от него почти не будет эффекта. Это можно будет грубо исправить, если делать ещё паузу в один кадр между тёмными и светлыми.

Ладно, синтезируем, выходит 1171 ЛЭ. 4 ЛЭ сэкономили на разрядности счётчика, вместо 10 бит (чтобы вместить 720) стало всего 6 бит (чтобы вместить 43), остальное не поменялось.

Смотрим, что получается, когда мы обрабатываем 32-ю строку и пытаемся выдать задания уже на 33-ю:


Да, пока что честно дождались обработки всей строки (до 0x1F = 31), получили яркость 0xFFF (нулевую) и координату 0. Сравнили яркость с пороговой и прыгнули в @@MidCycle.

В этом самом месте пришёл строчный синхроимпульс, возвестив начало 33-й строки! И ещё спустя такт у нас появилось UFLO. То есть, уже даже задание ACQ HSync не стало дожидаться всего Back porch (238 тактов), ведь как только пришёл HSync, счётчик строк внутри GPU перешёл на 43-ю, и теперь условие завершения задания ВСЕГДА ВЫПОЛНЕНО.

Странно только: почему UFLO появился и сразу пропал?

Тем временем, мы сообразили, что и список ActivePoints у нас фактически пустой (ничего нет, кроме двух фиктивных), а значит пора выходить на @@FinalRange.



Успешно выдаём два задания на следующую, 33-ю строку, ACQ WholeRow и ACQ HSync, и как только мы их выдаём - сразу же "загорается" UFLO, а спустя несколько тактов и OFLO. Именно такую картину мы ожидали увидеть: GPU "слетел с катушек" и пытается снять по одному заданию за такт и положить по одному новому результату за такт, из-за чего входной буфер улетает в UFLO, а выходной в OFLO.

Но на скриншоте видно, как всё снова "успокаивается"...

Тем временем, QuatCore не торопясь проверяет номер строки, решает ещё немножко поработать, прибавляет единицу к номеру строки и уходит в начало цикла.


Запрашиваются параметры очередного отрезка - и ответ получаем мгновенно, поскольку буфер весь забит, и всё сплошь нулями. И всё продолжается, на этот раз без длительных ожиданий на отправке задания или на приёме результатов. Именно в этих местах раньше обеспечивалась "синхронизация" QuatCore с входными данными, теперь её нет! Так и задумано: мы можем в максимально сжатые сроки "оттарабанить" пустые строки, с единственной целью перецепить все оставшиеся точки из ActivePoints в AllPoints.

В целом, всё работает ровно как надо. Странное поведение UFLO и OFLO, которые время от времени "гаснут", объясняется "Undefined behaviour" нашего входного буфера, см FIFO на ЛЭ, работа над ошибками:



Если мы непрерывно "дергаем" rdreq, то на входы ENA ВСЕХ регистров данных подаётся единица, т.е они АКТИВНЫ. При этом, если буфер пустой (а это так), на входы LOAD ВСЕХ регистров также подаётся единица, т.е хотя запроса на запись не поступает, они тем не менее что-то записывают из шины данных! Мотивация: в "пустых" ячейках имеет право находиться вообще что угодно, так зачем усложнять логику?

В результате, на буфер непрерывно заносится весь "мусор" из шины данных (с точки зрения GPU это мусор, потому что предназначен не ему), и как там оказывается единичный старший бит (т.е старший разряд в HEX-значении 8..F) - он воспринимается как команда VSync, который и портит нам всё веселье - теперь мы мирно ждём синхроимпульса.

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

Чуть снизим масштаб:


В кои-то веки обработка ОДНОЙ СТРОКИ умещается в один скриншот! Пусть эта строка тоже уже "фиктивная". На неё сейчас уходит 39 тактов, и от ширины реальной строки это значение не зависит, только от количества записей в списке ActivePoints.

Вообще, не уверен, что во всей этой затее с фиктивными нижними строками есть особенный смысл. Экономлю аж 8 строчек кода, или 16 байт:

;надо ещё все оставшиеся в ActivePoints точки "перецепить" в AllPoints
			Y	ActivePoints	;это заведомо фиктивная, и указывает либо на другую фиктивную, то ли на реальную
			X	AllPoints
			Y	[Y+k]			;это уже может быть реальная точка, а может быть правой фиктивной
@@Transfer:		ZAcc	RoundZero
			SUB	[Y+k]			;если [Y+k]=NULL, значит пора закругляться
			[SP++]	@@Transfer
			JL	ListOp
			NOP	[--SP]		;раз не было вызова - возвращаем стек в исходное состояние						


поставить их - и дело с концом. Ну ладно, ближе к делу придумаем.

И наконец, посмотрим, получится ли нам вообще выйти из цикла?


Да легко! А из него - сразу в бесконечный цикл @@endless, которым знаменуется окончание нашей программы.


С очередными странностями, но отработали кадр от начала до конца. Осталось посмотреть, что же из всего этого вышло?
Tags: ПЛИС, программки, работа, странные девайсы
Subscribe

  • Нахождение двух самых отдалённых точек

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

  • Слишком общительный счётчик

    Вчера я чуть поторопился отсинтезировать проект,параметры не поменял: RomWidth = 8 вместо 7, RamWidth = 9 вместо 8, и ещё EnableByteAccess=1, чтобы…

  • Балансируем конвейер QuatCore

    В пятницу у нас всё замечательно сработало на симуляции, первые 16 миллисекунд полёт нормальный. А вот прошить весь проект на ПЛИС и попробовать "в…

  • Огари разговаривают

    Сегодня по пути на работу встретил огарей прямо в Лосином острове, на берегу Яузы. Эти были на удивление бесстрашны, занимались своими делами, не…

  • Ковыряемся с сантехникой

    Наконец-то закрыл сколько-нибудь пристойно трубы, подводящие к смесителю, в квартире в Москве: А в воскресенье побывал на даче, там очередная…

  • Мартовское велосипедное

    Продолжаю кататься на работу и с работы на велосипеде, а также в РКК Энергию и на дачу. Хотя на две недели случился перерыв, очередная поломка,…

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments