Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Locating textures in the map without reloading #2

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

automatic9045
Copy link
Contributor

@automatic9045 automatic9045 commented Mar 10, 2022

This PR allows to locate textures in the map without reloading the scenario.

Changes

  • Add a TextureManager.PostInstantiateTexturePatcher class
    • This class allows replacing textures that are already registered.
    • After the scenario is loaded, this class will search for instances that define Texture of Material, and replace the target textures.
    • Searching instances takes some time (especially in large scenarios), so I also created a ProgressForm class, which displays the current progress of searching and replacing (see "UI Sample" for more details)
  • Move the existing feature to a TextureManager.PreInstantiateTexturePatcher class
  • Add a bool parameter to TextureManager.Initialize method
    • true or omitted -> search and replace textures that are already registered
    • false -> do not replace (same as the current version of DXDynamicTexture)
[DllExport(CallingConvention.StdCall)]
public static void Load() {
    TextureManager.Initialize(true);
    Texture = DynamicTexture.Create(@"clock_back_tex.png", 128, 128);
}
  • Update README according to the changes
  • And few changes

UI Sample

DXDTTest.mp4

NOTE: If all of TextureManager.Handles have been created (=TextureManager.Handles.All(h => h.IsCreated)), the search and replace process is skipped even if the TextureManager.Initialize method parameter is true.

@zbx1425
Copy link
Owner

zbx1425 commented Mar 11, 2022

Wow this is truly amazing! Thanks a lot for the impressive work! I've never thought of anything like this.

On the other hand, as BVE5 is not open-source, and the BVE5 community has a strong atmosphere of copyright, since a lot of reflection is used on the assembly of BVE in order to obtain the references, I am a bit afraid that some community members would consider it reverse-engineering, consider reverse-engineering copyright infringement, and accuse us for this.
From my knowledge it's not copyright infringement, at least not according to the US law, but I'm a bit concerned some community members without such knowledge would accuse us.
May I ask what do you think about this?

@automatic9045
Copy link
Contributor Author

automatic9045 commented Mar 12, 2022

Thank you for your reply!

since a lot of reflection is used on the assembly of BVE in order to obtain the references

Yes, but since the class searches for textures in a way that is independent of the BVE5/6 sources enough, I do not think this implementation is a copyright infringement.

The most important point is that the class searches for textures simply by enumerating instances created by BVE - it means largely independent of the structure of BVE's own classes.

List<object> ForEachMembers(
object parent, List<object> recognizedObjs, int maxNestCount,
Func<object, bool> targetObjSelector = null, Action<object> action = null, int defaultCapacity = 0) {
if (maxNestCount < 0) return new List<object>(0);
if (targetObjSelector == null) targetObjSelector = _ => false;
if (action == null) action = _ => { };
var parentType = parent.GetType();
var fields = parentType.GetFields(DefaultBindingFlags);
var unrecognizedObjs = fields
.Where(f => IsUniqueType(f.FieldType)) // Search only for fields of BVE's unique types; target textures are only instantiated with those fields
.Select(f => f.GetValue(parent))
.Flatten()
.Except(recognizedObjs) // Exclude objects already recognized
.Where(obj => obj != null && obj != parent);
var targetObjs = unrecognizedObjs.Where(targetObjSelector);
if (targetObjs.Any()) {
foreach (var obj in targetObjs) action(obj);
}
var objs = new List<object>(defaultCapacity <= 0 ? recognizedObjs.Count + unrecognizedObjs.Count() : defaultCapacity);
objs.AddRange(recognizedObjs);
objs.AddRange(unrecognizedObjs);
foreach (var obj in unrecognizedObjs) {
var childObjs = ForEachMembers(obj, objs, maxNestCount - 1, targetObjSelector, action, defaultCapacity + objs.Count);
objs.AddRange(childObjs.Except(objs));
if (childObjs.Any()) {
int progress = objs.Count / 200;
if (progress > 99) progress = 99;
progressForm.ReportProgress(progress, objs.Count, null);
}
}
return objs;
bool IsUniqueType(Type type) {
if (type.IsEnum) return false;
return type.Assembly == bveAssembly
|| (type.IsGenericType && type.GetGenericArguments().Any(IsUniqueType));
}
}

Here are the BVE5/6 sources on which the TextureManager.PostInstantiateTexturePatcher class depends:

The fact that "main form has a Time and Position form type field"

var allChildForms = ForEachMembers(mainForm, new List<object>(0), 0)
.FindAll(obj => obj is Form)
.ConvertAll(obj => (Form)obj);

The class searches for an instance of the Time and Position form from the main form of BVE, which is publicly accessible by Application.OpenForms[0].

The name of the Time and Position form

ForEachMembers(allChildForms.Find(f => f.Name == "SimOperationForm"), new List<object>(0), 4,

The class references the Name property of the form, "SimOperationForm." This is publicly accessible by Application.OpenForms[n].Name (n is some integer), not hidden.

A class that defines Mesh, Material, and Texture

I have described the process as "searching for objects that define textures," but more precisely, the class searches for objects that have the following structure:

(Target unique type object)
├ Mesh
└ IEnumerable<(Unique type object)>
  ├ (Unique type object)
  │ ├ Material
  │ └ Texture

“(Target unique object)” is a type defined in BVE, but it is very common to define a class that has such structure when make something with DirectX, especially when make in C# with SlimDX or SharpDX.

These are all parts that the class depends on the BVE sources. In my opinion, NONE of them are sufficient enough to be considered as reverse-engineering or copyright infringement (note: this does not mean that reverse-engineering infringes copyright).

@automatic9045
Copy link
Contributor Author

P.S.
I found an article written by a Japanese lawyer:
プログラムに関するリバースエンジニアリングの可否(平成30年著作権法改正)
("Is Reverse-engineering Legal or Illegal under Copyright Act?" in English)

Sorry for that the article is written all in Japanese. According to the article, a 2019 amendment to Japan's Copyright Act made it legal to reverse-engineer in order to develop some software that works with another software. There does not seem to be a legal issue in Japan, too!

@zbx1425
Copy link
Owner

zbx1425 commented Mar 14, 2022

Thanks a lot for your help!
I currently got occupied by some activities, so I'll take a look a few days layer. Sorry for the delay.

@zbx1425
Copy link
Owner

zbx1425 commented Mar 31, 2022

I'm terribly sorry to keep you waiting for such a long time.
I couldn't seem to find sufficient time to experiment with some ideas. And judging from your code I must admit that the quality of your code and your understanding of programming paradigms are far more surperior than mine.

Primarily speaking, I wonder if something could be done to further increase the speed of enumerating textures. It mainly breaks down to:

  • I've seen documents stating that emitting bytecode or using expression tree is faster than FieldInfo.GetValue() (https://mattwarren.org/2016/12/14/Why-is-Reflection-slow), will that help increase the speed?
  • LINQ has been used by a lot, will that hurt performance?
  • Cache BveModelInfoClassWrapper in a Dictionary<Type, BveModelInfoClassWrapper>, so that same accessor (FieldInfo or bytecode) can be used for each type?

I have wanted to test them out myself, but I am currently being occupied by some school activities, so I figured I'll just write them down for now. You can try these if you feel like to, or I'll get back to it later after the activities settle down, probably after a few months.
Sorry for the inconveniences.

@automatic9045
Copy link
Contributor Author

Don't worry about it! Give priority to schoolwork.
For starters, I changed some functions to not use LINQ, and the process is now about 7 times faster (83 seconds → 12 seconds). More refactoring might make it even faster, so I'll give it a try.

@zbx1425
Copy link
Owner

zbx1425 commented Mar 31, 2022

Thanks for the help!

I'd recommend replacing LINQ with plain for loops; some LINQ calls aren't very efficient in execution speed.
That blog entry also claims using Expression or generating bytecode will be much faster than FieldInfo.GetValue(), and a caching can be implemented so only one Expression or bytecode can be generated for each Type. I wonder if that extra performance is worth the work.

If it can be done fast enough, we can even remove Harmony and do all texture patching with this method, so that we can eliminate a (potentially instable) dependency.

@automatic9045
Copy link
Contributor Author

LINQ - Since all elements of IEnumerable<T> are finally instantiated, there wouldn't be any benefit of LINQ's lazy execution in this case. Another advantage of using LINQ is that it makes the code look more understandable, but the cost of the advantage, 70 seconds, was too heavy :(

Caching - I cannot understand why I didn't make those members static... Changed them to static and set only once.

Expression, generating bytecode - Now WIP!

For now, I've succeeded in reducing the processing time to 10-12 seconds. Hopefully I can reduce it further.

@automatic9045 automatic9045 force-pushed the dev/patch-map-textures-without-reloading branch from b6c950d to fc36f14 Compare April 1, 2022 13:06
@automatic9045
Copy link
Contributor Author

automatic9045 commented Apr 1, 2022

I refactored my code. Replaced some of the LINQ with plain loops, made the FieldInfos static and set only once, and changed a few other small things.
I also tried changing FieldInfo.GetValue to expression trees, but it was slower than using FieldInfo.GetValue (maybe because of my poor implementation).

Here is a comparison of processing speeds:

Total Time elapsed to patch the texture
Before e1cb61c 70498 - 89257ms (not tested)
After fc36f14 10167 - 12184ms 2625 - 2828ms
(Use expression trees) 10581 - 12536ms 2626 - 2925ms

* Tested five times for each case.
* Tested in the Uchibo Line default scenario with one additional structure to test texture-patching.
* Total is not very important; Texture instances seem to be loaded early.

I think 3 seconds to patch the texture is not too slow, what do you think? Do you think it is fast enough to remove Harmony? I would be glad to try it on your computer and get your opinion (no rush!).
And, if it is fast enough, it may be able to omit the progress form or change it to show only when in debug mode.

@zbx1425
Copy link
Owner

zbx1425 commented Aug 19, 2022

I'm so sorry that I sorta abandoned the project and kept you waiting for so long.

In view of the rapid development of your AtsEX plugin, which is of high quality and rich in feature, I think it already appears as a superset of DXDynamicTexture, thus it makes less sense to use DXDynamicTexture alone and it has now became proper to integrate DXDynamicTexture's entire functionality into AtsEX.
Using AtsEX alone will also provide convenience to train developers, and will avoid conflicts.

May I hear your opinion on this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants