Cheb's Game Engine

Планы, идеология, архитектура и т.п.

Модератор: Модераторы

Re: Cheb's Game Engine

Сообщение runewalsh » 19.06.2016 21:57:13

>а если средствами RTL?
Совсем не то, у автора аналог SetThreadStackGuarantee из WinAPI.
Аватара пользователя
runewalsh
энтузиаст
 
Сообщения: 578
Зарегистрирован: 27.04.2010 00:15:25

Re: Cheb's Game Engine

Сообщение скалогрыз » 19.06.2016 22:27:49

runewalsh писал(а):Совсем не то, у автора аналог SetThreadStackGuarantee из WinAPI.

типа, как выделить стэк для потока, больше чем разрешено потоку использовать?
... а интересно под Win9x такое вообще было (поминуя о том, что поддержку Win9x выкинули сравнительно недавно)

такое если и колдовать с помощью RTL, то с написанием своего ThreadManager-а.
Либо объявлять что-нить вроде "threadvar reserve: array [0..X] of byte;" и опираться на него.
скалогрыз
долгожитель
 
Сообщения: 1803
Зарегистрирован: 03.09.2008 02:36:48

Re: Cheb's Game Engine

Сообщение runewalsh » 19.06.2016 22:54:12

Нет, резерв стека, предназначенный для обработки ошибки переполнения стека. Если знаешь, что делаешь, то можно и вообще полностью восстановиться и продолжить выполнение (не уверен, сработает ли в FPC _resetstkoflw из msvcrt.dll — если она только восстанавливает сторожевую страницу, то сработает, а если не только, то хз).
Аватара пользователя
runewalsh
энтузиаст
 
Сообщения: 578
Зарегистрирован: 27.04.2010 00:15:25

Re: Cheb's Game Engine

Сообщение Mirage » 19.06.2016 23:31:55

Если надо предотвратить бесконечную рекурсию, то проще ведь считать кол-во рекурсивных вызовов, пробрасывая соответствующий параметр.
Mirage
энтузиаст
 
Сообщения: 881
Зарегистрирован: 06.05.2005 20:29:07
Откуда: Russia

Re: Cheb's Game Engine

Сообщение скалогрыз » 19.06.2016 23:32:46

runewalsh писал(а):Нет, резерв стека, предназначенный для обработки ошибки переполнения стека.

ну это точно не то, что Cheb хочет, ему отладочную информацию подавай.

Кстати, в той же статье по SetThreadStackGuarantee, сказано, что возможность восстановления от stack overflow - языкозависимая,
и похоже, что FPC-RTL на неё не способен (последний абзац так и пишет - терминируйтесь и всё тут!).

Добавлено спустя 39 секунд:
Mirage писал(а):Если надо предотвратить бесконечную рекурсию, то проще ведь считать кол-во рекурсивных вызовов, пробрасывая соответствующий параметр.
а как это поможет, если кол-во вызовов зависит от данных?

если уж действительно бояться за рекурисю и стек, то мне кажется проще делать проверку перед очередным вызовом, вроде
Код: Выделить всё
  if PtrUInt(sptr) - PtrUInt(StackBottom) < ALLOWED_STACKRESERVE then
    raise EStackTooclose.Create('paranoid error')
    // ну или halt(), exit() по- вкусу
  else
    Recurse(params);
скалогрыз
долгожитель
 
Сообщения: 1803
Зарегистрирован: 03.09.2008 02:36:48

Re: Cheb's Game Engine

Сообщение Cheb » 20.06.2016 00:08:23

резерв стека, предназначенный для обработки ошибки переполнения стека.

ну это точно не то, что Cheb хочет, ему отладочную информацию подавай.

Именно то, поскольку при переполнении стека не будет никакой отладочной информации, все мои самоотладочные городушки жрут много стека и кучи.

а как это поможет, если кол-во вызовов зависит от данных?

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

Вообще, этой ф-ии достаточно. Я могу тупо запомнить позицию стека при запуске потока и поднимать ошибку если, скажем, чеперси углубится от неё дальше, чем на 16 мегабайт.
А то полная бредятина сейчас, предел стека выставлен 160 мегабайт, наплодил четыре потока - и в 32 битном адресном пространстве уже тесно.
А на raspberry Pi 2 что делать буду? Там памяти один гиг вообще под всё, включая текстуры и операционку.

З.Ы. Но это всё тоже от паранойи. Я должен сделать так, чтобы 99% объектов хранились в массивах, и не использовать цепочки из указателей на следующего, тогда глубина рекурсии выйдет смешной.
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 994
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение скалогрыз » 20.06.2016 00:51:04

Cheb писал(а):Вообще, этой ф-ии достаточно. Я могу тупо запомнить позицию стека при запуске потока и поднимать ошибку если, скажем, чеперси углубится от неё дальше, чем на 16 мегабайт

а зачем самому запоминать? StackBottom StackTop
где StackBottom = StackTop - StackLength

ЗЫ: почему-то сказано что StackTop "contains the top of the stack for the current process", хотя он threadvar (подразумевая contains the top of the stack for the current thread?)

Добавлено спустя 26 минут 27 секунд:
скалогрыз писал(а):ЗЫ: почему-то сказано что StackTop "contains the top of the stack for the current process", хотя он threadvar (подразумевая contains the top of the stack for the current thread?)

я не прав. StackTop, StackBottom не инизиализированы на запуске потока, и к потоку никакого отношения не имеют. Опираться на них в многопоточном приложении нельзя... во всяком случае на 2.6.4
Код: Выделить всё
program project1;
{$mode delphi}{$H+}
var
  ttop, tbottom, tlength : PtrUInt;

function TestStack(parameter : pointer) : ptrint;
begin
  ttop:=PtrUInt(StackTop);
  tbottom:=PtrUInt(Stackbottom);
  tlength:=StackLength;
  Result:=555;
  EndThread(555);
end;

var
  tid : TThreadID;
  rid : TThreadID;
  res  : integer;

begin
  writeln('stack top    = ', PtrUInt(StackTop));
  writeln('stack bottom = ', PtrUInt(StackBottom));
  writeln('stack length = ', StackLength);

  tid:=0;
  rid:=BeginThread(nil, 1024*1024, @TestStack, nil, 0, tid);
  res:=WaitForThreadTerminate(rid, -1);
  writeln(res);

  writeln('t stack top    = ', PtrUInt(ttop));
  writeln('t stack bottom = ', PtrUInt(tbottom));
  writeln('t stack length = ', tlength);
end.


Добавлено спустя 4 часа 21 минуту 14 секунд:
скалогрыз писал(а):StackTop, StackBottom не инизиализированы на запуске потока, и к потоку никакого отношения не имеют. Опираться на них в многопоточном приложении нельзя... во всяком случае на 2.6.4

а в 3.0.0 почти работает.
StackTop и StackBottom инициализируются до значний соответствующих потоку, НО StackLength устанвливается в размер общего стэка, а не тот который указан при создании потока. и StackBottom следует StackLength.
скалогрыз
долгожитель
 
Сообщения: 1803
Зарегистрирован: 03.09.2008 02:36:48

Re: Cheb's Game Engine

Сообщение Cheb » 20.06.2016 11:29:17

а зачем самому запоминать? StackBottom StackTop
где StackBottom = StackTop - StackLength

Пробовал, убедился, что они возвращают шум океанов Марса, плюнул на них.

а в 3.0.0 почти работает.

Мило, но у меня одна из целей - сохранение компилируемости 2.6.4
- 3.0.0 крашится при попытке сборки на нём (давно не проверял, не до того)
- в Debian сейчас текущая версия - 2.6.4, что, как бы, намекает.
- на Raspberry Pi 2 - та же Дебиан, и хрен я буду 3.0.0 для неё добывать

кагбе:
Код: Выделить всё
//per-unit conditional defines that must be uniform across many units.
{$if FPC_FULLVERSION>=20700}
  {$define che_unicode}
{$endif}

Код: Выделить всё
type
{$include un_globaldefs.h}

{$ifdef che_unicode}
  {$if FPC_FULLVERSION<30000}
    {$fatal Impossible to use this compiler version: RTL is not unicode} //The sad truth
  {$endif}

  { modeswitch unicodestrings} //is in the main project file

  { warn IMPLICIT_STRING_CAST Error}
  {$warn IMPLICIT_STRING_CAST_LOSS Error}
  { warn EXPLICIT_STRING_CAST Error}
  {$warn EXPLICIT_STRING_CAST_LOSS Error}
  { if FPC_FULLVERSION<=40000} //***TODO Replace with version in which it is fixed
     {$define fix_fpc3_unicode} // Use custom mods of TFileStream, TStringList and TIniFile
  { endif}

  AnsiString1251 = type AnsiString(1251);
  TFileNameString = UnicodeString;
  PFileNameChar = PUnicodeChar;
{$else}
  //Legacy Free Pascal below 3.0
  RawByteString = AnsiString;
  AnsiString1251 = AnsiString;
  {$ifdef windows}
    //no unicode support for file names
    TFileNameString = AnsiString;
    PFileNameChar = PAnsiChar;
  {$else}
    TFileNameString = Utf8String;
    PFileNameChar = PAnsiChar;
  {$endif}
{$endif}


Причём, заплатки для их специфической кривизны у меня есть и для 2.6.4 (не освобождает хендл если он < 5)
Код: Выделить всё
{
  This is a permanent fix for the broken RTL of FPC 2.6.4
  See http://bugs.freepascal.org/view.php?id=27221

  This fix is required to get Platinum level of Wine compatibility
}

{$ifndef windows}
  {$fatal Windows ONLY}
{$endif}


type


  TFileStream = class(classes.TFileStream)
  public
    destructor Destroy; override;
  end;

  //The rest of the classes are dragged along by the need to base them on the fixed TFileStream


  TStringList = class(classes.TStringList)
  public
    procedure LoadFromFile(const FileName: AnsiString);
    procedure SaveToFile(const FileName: AnsiString);
  end;

  TIniFileKey = class
  Private
    FIdent: Utf8String;
    FValue: Utf8String;
  public
    constructor Create(const AIdent, AValue: Utf8String);
    property Ident: Utf8String read FIdent write FIdent;
    property Value: Utf8String read FValue write FValue;
  end;

  TIniFileKeyList = class(TList)
  private
    function GetItem(Index: integer): TIniFileKey;
    function KeyByName(const AName: Utf8String; CaseSensitive : Boolean): TIniFileKey;
  public
    destructor Destroy; override;
    procedure Clear; override;
    property Items[Index: integer]: TIniFileKey read GetItem; default;
  end;

  TIniFileSection = class
  private
    FName: Utf8String;
    FKeyList: TIniFileKeyList;
  public
    Function Empty : Boolean;
    constructor Create(const AName: Utf8String);
    destructor Destroy; override;
    property Name: Utf8String read FName;
    property KeyList: TIniFileKeyList read FKeyList;
  end;

  TIniFileSectionList = class(TList)
  private
    function GetItem(Index: integer): TIniFileSection;
    function SectionByName(const AName: Utf8String; CaseSensitive : Boolean): TIniFileSection;
  public
    destructor Destroy; override;
    procedure Clear;override;
    property Items[Index: integer]: TIniFileSection read GetItem; default;
  end;

  { TCustomIniFile }

  TCustomIniFile = class
  Private
    FFileName: Utf8String;
    FSectionList: TIniFileSectionList;
    FEscapeLineFeeds: boolean;
    FCaseSensitive : Boolean;
    FStripQuotes : Boolean;
  public
    constructor Create(const AFileName: AnsiString; AEscapeLineFeeds : Boolean = False); virtual;
    destructor Destroy; override;
    function SectionExists(const Section: Utf8String): Boolean; virtual;
    function ReadString(const Section, Ident, Default: Utf8String): Utf8String; virtual; abstract;
    procedure WriteString(const Section, Ident, Value: String); virtual; abstract;
    function ReadInteger(const Section, Ident: Utf8String; Default: Longint): Longint; virtual;
    procedure WriteInteger(const Section, Ident: Utf8String; Value: Longint); virtual;
    function ReadInt64(const Section, Ident: Utf8String; Default: Int64): Longint; virtual;
    procedure WriteInt64(const Section, Ident: Utf8String; Value: Int64); virtual;
    function ReadBool(const Section, Ident: Utf8String; Default: Boolean): Boolean; virtual;
    procedure WriteBool(const Section, Ident: Utf8String; Value: Boolean); virtual;
    function ReadDate(const Section, Ident: Utf8String; Default: TDateTime): TDateTime; virtual;
    function ReadDateTime(const Section, Ident: Utf8String; Default: TDateTime): TDateTime; virtual;
    function ReadFloat(const Section, Ident: Utf8String; Default: Double): Double; virtual;
    function ReadTime(const Section, Ident: Utf8String; Default: TDateTime): TDateTime; virtual;
    function ReadBinaryStream(const Section, Name: Utf8String; Value: TStream): Integer; virtual;
    procedure WriteDate(const Section, Ident: Utf8String; Value: TDateTime); virtual;
    procedure WriteDateTime(const Section, Ident: Utf8String; Value: TDateTime); virtual;
    procedure WriteFloat(const Section, Ident: Utf8String; Value: Double); virtual;
    procedure WriteTime(const Section, Ident: Utf8String; Value: TDateTime); virtual;
    procedure WriteBinaryStream(const Section, Name: Utf8String; Value: TStream); virtual;
    procedure ReadSection(const Section: Utf8String; Strings: TStrings); virtual; abstract;
    procedure ReadSections(Strings: TStrings); virtual; abstract;
    procedure ReadSectionValues(const Section: Utf8String; Strings: TStrings); virtual; abstract;
    procedure EraseSection(const Section: Utf8String); virtual; abstract;
    procedure DeleteKey(const Section, Ident: String); virtual; abstract;
    procedure UpdateFile; virtual; abstract;
    function ValueExists(const Section, Ident: Utf8String): Boolean; virtual;
    property FileName: Utf8String read FFileName;
    property EscapeLineFeeds: boolean read FEscapeLineFeeds;
    Property CaseSensitive : Boolean Read FCaseSensitive Write FCaseSensitive;
    Property StripQuotes : Boolean Read FStripQuotes Write FStripQuotes;
  end;

  { TIniFile }

  TIniFile = class(TCustomIniFile)
  Private
    FStream: TStream;
    FCacheUpdates: Boolean;
    FDirty : Boolean;
    FBOM : String;
    procedure FillSectionList(AStrings: TStrings);
    Procedure DeleteSection(ASection : TIniFileSection);
    Procedure MaybeDeleteSection(ASection : TIniFileSection);
    procedure SetCacheUpdates(const AValue: Boolean);
  protected
    procedure MaybeUpdateFile;
    property Dirty : Boolean Read FDirty;
  public
    constructor Create(const AFileName: AnsiString; AEscapeLineFeeds : Boolean = False); overload; override;
    constructor Create(AStream: TStream; AEscapeLineFeeds : Boolean = False); overload;
    destructor Destroy; override;
    function ReadString(const Section, Ident, Default: Utf8String): Utf8String; override;
    procedure WriteString(const Section, Ident, Value: String); override;
    procedure ReadSection(const Section: Utf8String; Strings: TStrings); override;
    procedure ReadSectionRaw(const Section: Utf8String; Strings: TStrings);
    procedure ReadSections(Strings: TStrings); override;
    procedure ReadSectionValues(const Section: Utf8String; Strings: TStrings); override;
    procedure EraseSection(const Section: Utf8String); override;
    procedure DeleteKey(const Section, Ident: String); override;
    procedure UpdateFile; override;
    property Stream: TStream read FStream;
    property CacheUpdates : Boolean read FCacheUpdates write SetCacheUpdates;
  end;

  TMemIniFile = class(TIniFile)
  public
    constructor Create(const AFileName: AnsiString; AEscapeLineFeeds : Boolean = False); overload; override;
    procedure Clear;
    procedure GetStrings(List: TStrings);
    procedure Rename(const AFileName: Utf8String; Reload: Boolean);
    procedure SetStrings(List: TStrings);
  end;


и для 3.0.0 (RTL уже юникодная, а классы - ещё нет)
Код: Выделить всё
{
  This is a temporary fix for the classes unit for FPC 3.0.0
}
type
  TFileStream = class(classes.THandleStream)
  Private
    FFileName : UnicodeString;
  public
    constructor Create(const AFileName: UnicodeString; Mode: Word);
    constructor Create(const AFileName: UnicodeString; Mode: Word; Rights: Cardinal);
    destructor Destroy; override;
    property FileName : UnicodeString Read FFilename;
  end;


  {  utf-8 string list}

  TStringList = class(classes.TStringList)
  public
    procedure LoadFromStream(Stream: TStream); override;
    procedure SaveToStream(Stream: TStream); override;
    procedure LoadFromFile(const FileName: UnicodeString);
    procedure SaveToFile(const FileName: UnicodeString);
  end;


  { utf-8 ini file }

  TIniFileKey = class
  Private
    FIdent: Utf8String;
    FValue: Utf8String;
  public
    constructor Create(const AIdent, AValue: Utf8String);
    property Ident: Utf8String read FIdent write FIdent;
    property Value: Utf8String read FValue write FValue;
  end;

  TIniFileKeyList = class(TList)
  private
    function GetItem(Index: integer): TIniFileKey;
    function KeyByName(const AName: Utf8String; CaseSensitive : Boolean): TIniFileKey;
  public
    destructor Destroy; override;
    procedure Clear; override;
    property Items[Index: integer]: TIniFileKey read GetItem; default;
  end;

  TIniFileSection = class
  private
    FName: Utf8String;
    FKeyList: TIniFileKeyList;
  public
    Function Empty : Boolean;
    constructor Create(const AName: Utf8String);
    destructor Destroy; override;
    property Name: Utf8String read FName;
    property KeyList: TIniFileKeyList read FKeyList;
  end;

  TIniFileSectionList = class(TList)
  private
    function GetItem(Index: integer): TIniFileSection;
    function SectionByName(const AName: Utf8String; CaseSensitive : Boolean): TIniFileSection;
  public
    destructor Destroy; override;
    procedure Clear;override;
    property Items[Index: integer]: TIniFileSection read GetItem; default;
  end;

  { TCustomIniFile }

  TCustomIniFile = class
  Private
    FFileName: Utf8String;
    FSectionList: TIniFileSectionList;
    FEscapeLineFeeds: boolean;
    FCaseSensitive : Boolean;
    FStripQuotes : Boolean;
  public
    constructor Create(const AFileName: UnicodeString; AEscapeLineFeeds : Boolean = False); virtual;
    destructor Destroy; override;
    function SectionExists(const Section: Utf8String): Boolean; virtual;
    function ReadString(const Section, Ident, Default: Utf8String): Utf8String; virtual; abstract;
    procedure WriteString(const Section, Ident, Value: String); virtual; abstract;
    function ReadInteger(const Section, Ident: Utf8String; Default: Longint): Longint; virtual;
    procedure WriteInteger(const Section, Ident: Utf8String; Value: Longint); virtual;
    function ReadInt64(const Section, Ident: Utf8String; Default: Int64): Longint; virtual;
    procedure WriteInt64(const Section, Ident: Utf8String; Value: Int64); virtual;
    function ReadBool(const Section, Ident: Utf8String; Default: Boolean): Boolean; virtual;
    procedure WriteBool(const Section, Ident: Utf8String; Value: Boolean); virtual;
    function ReadDate(const Section, Ident: Utf8String; Default: TDateTime): TDateTime; virtual;
    function ReadDateTime(const Section, Ident: Utf8String; Default: TDateTime): TDateTime; virtual;
    function ReadFloat(const Section, Ident: Utf8String; Default: Double): Double; virtual;
    function ReadTime(const Section, Ident: Utf8String; Default: TDateTime): TDateTime; virtual;
    function ReadBinaryStream(const Section, Name: Utf8String; Value: TStream): Integer; virtual;
    procedure WriteDate(const Section, Ident: Utf8String; Value: TDateTime); virtual;
    procedure WriteDateTime(const Section, Ident: Utf8String; Value: TDateTime); virtual;
    procedure WriteFloat(const Section, Ident: Utf8String; Value: Double); virtual;
    procedure WriteTime(const Section, Ident: Utf8String; Value: TDateTime); virtual;
    procedure WriteBinaryStream(const Section, Name: Utf8String; Value: TStream); virtual;
    procedure ReadSection(const Section: Utf8String; Strings: TStrings); virtual; abstract;
    procedure ReadSections(Strings: TStrings); virtual; abstract;
    procedure ReadSectionValues(const Section: Utf8String; Strings: TStrings); virtual; abstract;
    procedure EraseSection(const Section: Utf8String); virtual; abstract;
    procedure DeleteKey(const Section, Ident: String); virtual; abstract;
    procedure UpdateFile; virtual; abstract;
    function ValueExists(const Section, Ident: Utf8String): Boolean; virtual;
    property FileName: Utf8String read FFileName;
    property EscapeLineFeeds: boolean read FEscapeLineFeeds;
    Property CaseSensitive : Boolean Read FCaseSensitive Write FCaseSensitive;
    Property StripQuotes : Boolean Read FStripQuotes Write FStripQuotes;
  end;

  { TIniFile }

  TIniFile = class(TCustomIniFile)
  Private
    FStream: TStream;
    FCacheUpdates: Boolean;
    FDirty : Boolean;
    FBOM : String;
    procedure FillSectionList(AStrings: TStrings);
    Procedure DeleteSection(ASection : TIniFileSection);
    Procedure MaybeDeleteSection(ASection : TIniFileSection);
    procedure SetCacheUpdates(const AValue: Boolean);
  protected
    procedure MaybeUpdateFile;
    property Dirty : Boolean Read FDirty;
  public
    constructor Create(const AFileName: UnicodeString; AEscapeLineFeeds : Boolean = False); overload; override;
    constructor Create(AStream: TStream; AEscapeLineFeeds : Boolean = False); overload;
    destructor Destroy; override;
    function ReadString(const Section, Ident, Default: Utf8String): Utf8String; override;
    procedure WriteString(const Section, Ident, Value: String); override;
    procedure ReadSection(const Section: Utf8String; Strings: TStrings); override;
    procedure ReadSectionRaw(const Section: Utf8String; Strings: TStrings);
    procedure ReadSections(Strings: TStrings); override;
    procedure ReadSectionValues(const Section: Utf8String; Strings: TStrings); override;
    procedure EraseSection(const Section: Utf8String); override;
    procedure DeleteKey(const Section, Ident: String); override;
    procedure UpdateFile; override;
    property Stream: TStream read FStream;
    property CacheUpdates : Boolean read FCacheUpdates write SetCacheUpdates;
  end;

  TMemIniFile = class(TIniFile)
  public
    constructor Create(const AFileName: UnicodeString; AEscapeLineFeeds : Boolean = False); overload; override;
    procedure Clear;
    procedure GetStrings(List: TStrings);
    procedure Rename(const AFileName: Utf8String; Reload: Boolean);
    procedure SetStrings(List: TStrings);
  end;
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 994
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение Cheb » 22.06.2016 09:19:17

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

1. У всех ассетов в маске установлено ASSET_MASK
2. У всех ассетов есть уникальный хеш
3. При сохранении сессии, ассеты добавляют себя в отдельный список. Для этого им дадена BeforeSaving (Чеперси, кстати, применяет хитрожопую оптимизацию, вызывая этот виртуальный метод только если он у класса отличается от адреса родительского метода-пустышки)
4. После сохранения сессии, этот список сохраняется в отдельный TMemoryStream экзешника-матки (доступ через враппер TMotherStream) по маске ASSET_MASK - т.е. в этой сохранёнке любые ссылки на не-ассеты будут NIL. См. mo_assets_stor.inc
5. DLL модуля тупо выгружается - безо всяких вызовов деструкторов, поскольку её диспетчер памяти по любому умрёт. (себе на заметку: проверить не забыл ли удаление ассетов если DLL выгружается из-за того, что программа собирается закрываться)
6. Имеем: DLL выгружена, все ассеты с о всеми их хендлами текстур и прочим утрамбованы в памяти екзешника-матки.
7. После загрузки DLL модуля, вызывается недо-конструктор class procedure TLogic.CreateOrLoad, который сначала получает от матки список сохранённых ассетов
8. Как часть процесса загрузки, каждому классу ассета дадена AfterLoading, внутре которой он ищет свой эквивалент в списке по хешу, и найдя, пожирает его (TGenericAsset.Devour), оставляя пустую выеденную шкурку, у которой можно безопасно вызывать деструктор. То есть, каждый класс ассета должен ещё иметь виртуальный метод для грамотного пожирания собрата, что в случае составных (менеджед фбо владеет текстурой и буфером глубины) превращается в попаболь. Но мы не можем просто взять тот, который загружен с матки: у него сброшены все указатели на не-ассеты.
9. После загрузки производится подчистка, все в списке удаляются. Если их никто не сожрал - при этом удаляется и хендл и прочая.

Код: Выделить всё
  function TGenericAsset.Devour(c: TGenericAsset): boolean;
  begin
    f_props:= c.AssetProperties;
    f_hash:= c.Hash;
    Result:= true;
  end;

  function THandleAsset.Devour(c: TGenericAsset): boolean;
  begin
    Self._handle:= (c as THandleAsset).Handle;
    Result:= inherited Devour(c);
  end;

    procedure TGenericAsset.AfterLoading;
  var
    Counterpart: TGenericAsset;
  begin
    if gv_RetrievingFromMother then Exit; //loading stored assets
      //from module ci stream. Do nothing;

    if Assigned(gv_RetrievedAssets)
      then Counterpart:= gv_RetrievedAssets.GetByHash(@Hash)
      else Counterpart:= nil;
//if Assigned(Counterpart) then AddLog('** %0', [TellInvalidAsset(Counterpart)]);
//bgSay(PervertedFormat('* %0.AfterLoading :  Counterpart=%1', [string(Self.ClassName), pointer(counterpart)]));
//if Assigned(counterpart) then
//bgSay(PervertedFormat('                     Counterpart=%0', [string(Counterpart.ClassName)]));

    if Assigned(Counterpart)
      //Counterpart.Exists
      //and Counterpart.IsValid //Fuck! It could be *not* valid due to lacking
        //some non-saveable derivative objects (like TMotherStream and such.
        //Because of that, Counterpart.Actualized() returns false.
        //Imma fucking idiot :(
      and (ap_actualized in Counterpart.AssetProperties)

    then begin
      if Mother^.Debug.Verbose then AddLog('    %0.AfterLoading: devouring the retrieved counterpart %1 with hash %2', [string(ClassName), pointer(Counterpart), HashToHex(Hash)]);
      if Devour(Counterpart) //copy its fields *including* the ap_actualized flag!
      then begin
        f_props-= [ap_unclaimed];
        if Mother^.Debug.Verbose then AddLog('    ..devoured');
        Counterpart.EraseContainer;
      end
      else begin
        if Mother^.Debug.Verbose then AddLog('    ..failed');
        EraseContainer; //handles and such are
        //  always zeroed out when loading session. Previously, this was done via
        //  skippable fields. But since I switched to using Chepersy for storing
        //  entire assets in the mother stream, that is just plain not possible.

        //actualization is now *stored* in the session as it is a part of a set
        //  (previously it wasn't because it was a skipped field).
        //EraseContainer erases it too.
      end;
    end
    else begin
      if Mother^.Debug.Verbose then begin
        if Assigned(Counterpart)
          then AddLog('    %0.AfterLoading: the counterpart for hash %1 is not valid (%2)',[string(ClassName), HashToHex(Hash), TellInvalidAsset(Counterpart)])
          else AddLog('    %0.AfterLoading: no counterpart retrieved for hash %1.',[string(ClassName), HashToHex(Hash)]);

      end;
      EraseContainer;
    end;

{Old version for comparison:
      pr: PAssetRecord;

      pr:= Mother^.AssetKeeper.Claim(@Hash);
      AfterEfCheck; //throw an exception if there was an error
      if Assigned(pr) then begin
        //  The handle could have changed, for example the texture
        //    we created during the yesterday session from the same image
        //    (thus the same hash) had different index in OpenGL.
        //  So we reload it with the value received from the mother module.
        _handle:= pr^.Handle;
        FActualized:= true;
      end;
}
//    end;
//bgsay('     4');
//    end;


    //Actualized() can be checking a shitload of other things
    //  besides (ap_actualized in f_props), so beware.
    if not Actualized then MyLogClass.AddNonActualizedAsset(Self);
//bgsay('     5');
  end;   
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 994
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение Cheb » 23.06.2016 09:37:30

Чуть не подложил себе страшный нежданчик.
А если ассет освободится в не основном потоке из-за того, что последняя ссылка на него была в логике?.. :shock:

Спешно допилил затычко:

Код: Выделить всё
function TChepersyObject._Release : longint;{$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF};
  begin
    if ((CpsMask and CPS_NO_INTERLOCK_MASK) > 0) and not ForceInterlock
    then begin
      Dec(f_RefCount); Result:= f_RefCount;
    end
    else
      Result:= interlockeddecrement(f_RefCount);

    if Result = 0 then Self.OnZeroReferenceCount;
  end;

procedure TChepersyObject.OnZeroReferenceCount;
begin
  Self.Destroy;
end; 

  procedure TGenericAsset.OnZeroReferenceCount;
  begin
    if not Self.MustBeFreedFromMainThread()
      or (GetCurrentThreadId() = Mother^.State.MainThreadId)
    then inherited //cal Destroy
    else begin
      EnterWv(TAssetManager.ToDeleteInMainThreadCS); //wrapper to allow the thread load indicator profiling this
      if not Assigned(TAssetManager.ToDeleteInMainThread)
        then TAssetManager.ToDeleteInMainThread:= TAssetContainer.Create;
      TAssetManager.ToDeleteInMainThread.Add(Self); //refcount is 1 again
      TAssetManager.ToDeleteInMainThreadCS.Leave;
    end;
  end;

  function TGenericAsset.MustBeFreedFromMainThread: boolean;
  begin
    Result:= false
  end; 

  function TTexture.MustBeFreedFromMainThread: boolean; begin Result:= True end;   

  class procedure TAssetManager.Pulse;
  var
    q: qword;
    i, t, c : integer;
  begin
    if Assigned(ToDeleteInMainThread) then begin
      EnterWv(TAssetManager.ToDeleteInMainThreadCS);
      t:= Length(ToDeleteInMainThread.List);
      if (Mother^.Debug.Verbose) then begin
        AddLog('  Delayed deleting of %0 assets in the main thread...', [t]);
        Mother^.Timer.UsecDelta(@q); // Not free anymore. On an ARM platform it can take up to a microsecond.
      end;

      for i:= 0 to High(ToDeleteInMainThread.List) do
        ToDeleteInMainThread.List[i]:= nil; //will deadlock the CS if this is not the main thread

      if (Mother^.Debug.Verbose) then AddLog('  Deleted %0 delayed assets in %1µs',[t, round(Mother^.Timer.UsecDelta(@q))]);
      ToDeleteInMainThread:= nil;
      ToDeleteInMainThreadCS.Leave;
    end;


    //For *now*, just delete them at once, assuming that the cost of
    //  freeing whatever is negligible.
    //***TODO: benchmark and optimize the shit out of this if needed.

    if Assigned(Retrieved) then begin

      t:= Length(Retrieved.List);

      if (Mother^.Debug.Verbose) then begin
        AddLog('  Deleting %0 unclaimed assets...', [t]);
        Mother^.Timer.UsecDelta(@q); // Not free anymore. On an ARM platform it can take up to a microsecond.
      end;

      c:=0;
      for i:= 0 to High(Retrieved.List) do
        if Assigned(Retrieved.List[i]) then with Retrieved.List[i] do begin
          //if not Exists then continue;
          if Actualized
            then begin
              if (Mother^.Debug.Verbose) then AddLog('    ..deleting %0', [string(ClassName)]);
              c+= 1;
            end;
          Retrieved.List[i]:= nil;
        end;

      if (Mother^.Debug.Verbose) then AddLog('  Deleted %0 unclaimed assets in %1µs',[c, round(Mother^.Timer.UsecDelta(@q))]);
      Retrieved:= nil;
    end;
  end; 

Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 994
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение Cheb » 03.07.2016 18:30:26

Поток сознания:

Кроме того, немаловажно: хоть ассеты и сохраняются в сессию, при отсутствии полученного от матки экземпляра (а такое возможно *только* в пределах времени выполнения екзешника - т.е., фактически, служит для ускорения перезагрузки DLL в режиме разработчика И ускорения загрузки близкой по составу сохранёнки) - во всех остальных случаях они после загрузки будут очищены методом EraseContainer. Также, создаются они тоже пустыми и требуют актуализации.



[отступление]
Дальность видимости.
Дано: поле зрения - 120 х 67.5 градусов. Полный круг = 3 * 120 = 360, оно же 3 * 1920 = 5760 пикселов. Расстояние, на котором один блок размером с пиксел = 5760 / (Pi * 2) = 916 блоков = 586 метров (при размере блока 64 см). Такая детальность нахрен никому не спёрлась, поскольку у нас ещё есть дальняя зона, от границы видимости до горизонта, где ландшафт рендерится с переменной детализацией от обобщённого столбика на каждый чанк до одного треугольника на полукилометровый килочанк - фактически, вся планета всегда рендерится полностью. Смешивание зон плавное - т.е. там, где один пиксел на блок, будет практически невидимо на фоне дальней зоны.

Вывод: границу видимости не только можно, но и нужно придвинуть поближе.
один блок = два пиксела - 458 блоков / 293 метра
один блок = три пиксела - 306 блоков / 196 метров
Вот из этих двух уже можно выбирать, и сделать это придётся уже по результатам реальных тестов движка.
Кстати, похоже на то, как устроен Ace of Spades на максимальной дистанции видимости: предел тумана - примерно пол-карты, где большинство карт - 512х512 блоков. И то, оппоненты при этом выглядят муравьями даже в прицел снайперки.
[/отступление]




Связь между логикой (подмножеством которой является физика) и ассетами.

Хотя прямая связь возможна - это на уровне грязного хака (например, одноразовый элемент меню порождает в качестве своего задника TTextureFromImage, привязанную к coolbackground.jpg). Почему хак? Такая привязка или окажется вечной, никогда не меняясь с момента создания сессии, или логический объект обязан уметь приспосабливаться, проверяя, не поиеняли ли .jpg на .png и существует ли оная ещё.

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

Короче, без *многоэтажной* структуры прослоек тут не обойтись.

Разберём, для примера, чанк мира. На первый взгляд, его ассеты - это:
1. Меш
2. Текстура
3. нормалмапа
4. лайтмапа
Причём, всё устроено так хитро, что меш *не* перестраивается при изменениях освещения, только перерисовывается лайтмапа, причём не вся.

Но. Сильно пересечённая местность может содержать до 25 тысяч чанков в зоне видимости (я не рассматриваю здесь полную 250 тысяч абсолютного хаоса, такого движок не потянет и выполнит откат части чанков на примитивную форму, подробнее об этом позже).

25 тысяч переключений текстуры... Я как-то проводит тесты. Средняя видеокарта подавилась на четырёх тысячах, начала подвисать и лагать. Причём, неровно, эпилептически.

Откуда вылезает необходимость ещё одного слоя.

Мегатекстура - менеджер текстурных атласов, в которых чанки размещают свои текстурные развёртки. И чанки рендерятся в порядке группировки по текстурам.

Вершинные буферы - предположим, на 65535 вершин каждый, в которых меши размещают свои данные. По многу мешей на буфер.

[отступление]
Анизотропная фильтрация будет не использоваться, либо ограничена 2..4х, с проверкой и отказом запускаться, если включена в драйвере принудительно. Её роль возлагается на суперсэмплинг.
Мипмэппинг будет *только ручной*, на уровне динамического управления мегатекстурою.
[/отступление]

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

Почему не текстурные массивы: планируется совместитмость с OpenGL ES 2 с минимальными переделками.

[sanity check]
Примем минимальный размер развёртки на сторону блока за 4 пихсела. Как раз соответствует визуальному размеру блока в 3 пихсела на границе видимости.
Тогда мегатекстура 4096х4096 может содержать 1 миллион кусочков (это песец, крайний случай, всё в мыле). Делим на 3 текстуры - получаем где-то 300 тыщ граней. Разумно - где-то 100 тыщ, чтобы дать место более крупным для вблизи.
Теперь берём теоретически возможный крайний случай (шахматная клеточка из блоков) где число блоков = 3/4 * Pi * R^3, где R = 310 блоков. Получаем... мнээээ... Чё так толсто-то? :( 125 миллионов блоков х 5 граней = 625 миллионов. Ппц, всё умерло.
Вывод первый: система должна уметь давиться грамотно, переключая рендер части чанков на обобщённые столбики, как в дальней зоне.
Дубль второй, берём плоский круг радиусом с зону видимости и умножаем его площать в 5 раз, предполагая, что там пещеры и утёсы, но бОльшая часть мира таки монолитная и видимых поверхностей разумное количество. Получаем 5 * Pi * R^2 = полтора мильона :(
Да что ж так толсто то!!!Изображение
Окей, не сдаёмся.
1. Снижаем проекцию грани до 2 пихселов (чисто чтобы не мелькали белые подштанники при резком взгляде назад и всё всегда было покрыто.
2. Группируем этот "крайний мип" по текстурам вменяемого размера (скажем, 256х256), всегда группируя в одну текстуру пространственно близкие чанки
3. Группируем по нескольку чанков в один меш - скажем, для зоны дальше 100 метров, т.е. 7/8 их общего числа.
4. Оглядываемся назад и понимаем, что получилась полная муйня. Идея натянуть на мир мегатекстуру и сэкономить обновлении мешей чанков за счёт лайтмапы с треском пролетает, на low-end железе это всё подавится и умрёт.
Или?..
В классическом майнкрафте разумный предел видимости где-то 10 чанков был (160 блоков), при превышении FPS'ы проседали.
[/sanity check]

Так, а зачем мне вообще нужна мегатекстура? Для условно-неограниченных декалей. Элементы декора разные, уляпывание стен (см. Brutal DooM: стилистика замечательная, но тормозит страшно. А с мегатекстурой не тормозило бы: Изображение ) И для текстуринга, основанного на смешивании многих текстур, что даст полное отсутствие повторяемости, но дороговато в рилтайме.
Лайтмапы, опять же, для удешевления обновлений освещения.

Окей, как я могу увернуться от использования для диситанций от 100 до 200 метров? (самое геморное, там до усрачки микрокусочков)
1. Сделать каждую грань блока монотонной, неотекстуренной, сложив все декали). Т.е. хранить лишь цвет на вершину
2. Отказаться от правдоподобного освещения: хранить освещённость дневным светом как одно число на вершину (а освещённость дневным светом может меняться *только* при изменении геометрии), скомбинировав с deferred освещением для динамических источников. Всё это аккуратно прикрывается атмосферной дымкой.

В диапазоне, где один блок - от 6 до 3 пихселов, может прокатить.

Дубль 2, (песня) А я дятел, я дятел, я дуплистый такой...

Эти траханые голубем полтора миллиона - это по кругу. Во фрустум одновременно попадёт примерно треть, то есть 500 тыщ. Если отрешиться от фбо (у которого на большинстве железа довольно маленький лимит) и применять реально {мегатекстуру (на огле - 8192х8192 либо же 512х512х256, на глесе - всё грустно) и наполнять ея через glCopyTexSubImage... Стоп. Тогда почему не просто текстуры?

200 - 3
1.2 млн
100 - 6
350 тыс.
50 - 12
100 тыс.
25 - 24
24 тыс.
12 - 48
6 тыс.

P.S. Всё было б кучеряво, кабы не зум снайперки, подло приближающий это всё в 2 или же 3 раза. И ооооопаньки :(((

Вывод: воксели текстурятся обычным атласом (даже с мипами), до 3 текстур на треугольник, смешиваемых шейдером по выпуклости, зашитой в альфу. А наляпы всякие - отдельным слоем, который выдаётся только заляпанным вокселям, и только в радиусе 100м. Свет - тоже повершинный, ambient occlusion - в альфе.

Индивидуальный сурфейс-кеш - только для тетроксельных мешей, причём, только определённых классов (стены, двери, транспортные средствА). Тетроксельные меши древесных стволов и прочего подобного - текстурятся тем же сраным атласом, что и тризмоксели ландшафта.
Последний раз редактировалось Cheb 31.07.2018 23:12:15, всего редактировалось 1 раз.
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 994
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение скалогрыз » 03.07.2016 20:01:00

Cheb писал(а):Так, а зачем мне вообще нужна мегатекстура? Для условно-неограниченных декалей. Элементы декора разные, уляпывание стен (см. Brutal DooM: стилистика замечательная, но тормозит страшно. А с мегатекстурой не тормозило бы:

С моим устарвешим пониманием 3д движков - мне всегда думалось, что уляпывание стен рендерится последним, после отрисовки основной сцены (с z-buffer enabled).
При-этом вся кровь-кишки, дырочки от пуль, как раз собраны в одну текстуру. Не уверен, что термин "мега-текстуры" здесь подойдёт.
Но т.к. они все собраны воедино, проблемы с переключением текстуры нет.
скалогрыз
долгожитель
 
Сообщения: 1803
Зарегистрирован: 03.09.2008 02:36:48

Re: Cheb's Game Engine

Сообщение Cheb » 03.07.2016 22:31:05

Я просто перечитался Майка Абраша, его книга про то, как создавался рендер первого квейка, просто замечательная. И да, в первом квейке не было уляпывания стен кровью только от того, что мужики спешили. Там на деле для каждой стенки на лету создавалась уникальная текстура из наложенной базовой текстуры, предумноженной на лайтмапу. Декали добавить было бы - как два пальца об асфальт. Причём, на цене основного рендеринга вообще не сказалось бы (цена равна нулю), замедлилась бы только генерация этих динамических текстур - а она происходит не каждый кадр, только при заметном смещении камеры или изменении освещения. Кстати, внимательно присмотритесь - в первом и втором квейке пятна света от ракет движутся хорошо заметными скачками, кадров 5-10 в секунду, не больше. Потому что при этом даже при аппаратном 3d рендеринге куски лайтмапы заново генерируются на CPU и перезакачивается на видеокарту.

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

Придётся вертеться.
Во первых, делать развёртку наляпов частичной, для только там, где они есть. Да ещё приоритеты учитывать.
Во вторых, у меня всё равно будет не чистое майнкрафтоподобие, а поддержка нерегулярной сетки с частичным заполнением блока по вертикали и "бутербродами" из слоистых многослойных блоков, когда поверх 17см. камня может идти 3см. песка, воды - или тех же "кровькишки", которые при достаточном накоплении тупо подцепят свою текстуру к верхней поверхности блока, и никакой дополнительный слой уляпа не понадобится.

Кстааати, а это вообще идея. Малые количества рисовать динамическими штуками типа частиц, причём анимированными и временными - типа, "стекло со стены на пол". А если угваздано вусмерть - просо переключать текстуру, которая будет в том же атласе.

С дырками от пуль, правда, такое не проканает.

Подробнее - когда обкатаю классы основных ассетов (меши, базовый атлас и иже с ними).

З.Ы. Чего я *не* собираюсь делать - это тупой клон майнкрафта, где нужно бегать с киркой и возводить домики по кубику. Управление будет "ну-ка, големы, быстро воткнули мне типовой домик вот здесь" (привет от RTS) и "взял BFG и всё расхреначил" (привет от FPS). Ну, максимум - ещё лопата для ковыряния ямок.
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 994
Зарегистрирован: 06.06.2005 15:54:34

Re: Cheb's Game Engine

Сообщение скалогрыз » 03.07.2016 22:47:57

Cheb писал(а):...как создавался рендер первого квейка... И да, в первом квейке не было уляпывания стен кровью только от того, что мужики спешили. ...на лету создавалась уникальная текстура из наложенной базовой текстуры, предумноженной на лайтмапу. Декали добавить было бы - как два пальца об асфальт. Причём, на цене основного рендеринга вообще не сказалось бы (цена равна нулю), замедлилась бы только генерация этих динамических текстур - а она происходит не каждый кадр, только при заметном смещении камеры или изменении освещения.

Мне кажется в больших комнатах (с большим количеством гуро), всё бы начинало либо тормозить, либо чудесным образом пропадать.
Память-то всё-таки не бесконечная. Ещё хуже, если драйвер, таки решит что бесконечная, и начнёт текстуры таскать из GPU в RAM.

И даже если, камера/освещения не меняется, то если учесть, что на каждую каплю пролитой крови своя текстура, то вновь поднимается проблема слишком большого переключения текстур.

Cheb писал(а):Управление будет "ну-ка, големы, быстро воткнули мне типовой домик вот здесь" (привет от RTS)

Напоминает заставку в Battle Zone 1, где бравый американский астронафт, ловко командует своим подчинённм турелькам голосом: "займи позицию между турелькой 1 и западным входом". В самой игре, всё оказалось намного прозаичнее, т.к. приходилось тыкать юнит в конкретную точку на земле. В разгар боя это напрягало, и тактическая часть игры быстро отметалась в пользу 3d-шутера... Тогда тактические паузы были не модны :)
скалогрыз
долгожитель
 
Сообщения: 1803
Зарегистрирован: 03.09.2008 02:36:48

Re: Cheb's Game Engine

Сообщение Cheb » 03.07.2016 23:04:56

В режиме сетевого шутера никаких големов не будет. Игрок будет иметь возможность спамить типовыми конструкциями типа "стенка с бойницами", "сегмент моста с перилами" и т.п. покуда заряда хватит. Это цельнотяг от AoS, где за 15-минутный тайм в командном дезматче, бывало, успевали высрать целую вавилонскую башню, окутанную балкончиками для кемперов и соединённую со спавном команды небесными мостами. А противоположная команда успевала её снести к ядрене фене.

Простейшая, казалось бы, идея, но я больше *нигде* её не встречал. Хотя, казалось бы, майнкрафтошутеров - легион. Но все - тактическое говно. Такого fast and Furious строительства/сноса нигде нет.

А так - да, планируется после шутера делать RPG на манер хранителя подземелий.

в больших комнатах (с большим количеством гуро), всё бы начинало либо тормозить, либо чудесным образом пропадать.

В брутальном думе - да, тормозит. Но если запечь всю кровищу в одну текстуру - то как раз не будет. См. Alien Shooter 2. Изометрия, но трупов иногда по колено чавкало. И ещё колёсами джипа размазывалось, оставляя следы протектора.
Аватара пользователя
Cheb
энтузиаст
 
Сообщения: 994
Зарегистрирован: 06.06.2005 15:54:34

Пред.След.

Вернуться в Разработки на нашем сайте

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и гости: 37

Рейтинг@Mail.ru